Import 'ipp' crate
Request Document: go/android-rust-importing-crates
For CL Reviewers: go/android3p#cl-review
Bug: 402933191
Test: m libipp
Change-Id: I04ab04adc4ad4a367691e4a96f0a81d7eb59ead0
diff --git a/crates/ipp/.android-checksum.json b/crates/ipp/.android-checksum.json
new file mode 100644
index 0000000..2635a5f
--- /dev/null
+++ b/crates/ipp/.android-checksum.json
@@ -0,0 +1 @@
+{"package":null,"files":{".cargo-checksum.json":"55db6339bc41d12f40c280cbbeb90d8234fcb2505b323c35f2a13d7605390acc","Android.bp":"c436d31352e460b05b372af27cabd911abe6c968739068275c34f77765c25665","Cargo.toml":"5a8d9dc131c4a5bf228ade9a943bb34e8e3957fa0988277c7bd9ae69e6c4c931","LICENSE":"08104e056668223e57f38f83c6c33ed0af9725350a1c5c2aea015c8e5447a5fd","METADATA":"f31dbaee517dd7732a2e1af7a5e9dba6361e192bd3e9f804058b40ef4f73d2ab","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","README.md":"7a76e97a0bc738d82276b38f880cd112f1e21f00adc2a66c23dbc9d396de31b0","TEST_MAPPING":"9e8f49c038111726c8ee932cd96366e225b7b7010a1a454a4450a15bc1189f30","cargo_embargo.json":"fd790b4d2ef154f01185fb7cb018a6222e5e4d3fd008601c209e60d9f599bf63","patches/LICENSE.patch":"9a3abe19228cdff5436891287af162ffc05d7cb140f2a6e8130141791e52e75c","src/attribute.rs":"4eaf004d7b15585d6cdae8e7f6d9d52749f32d0bc637be201d287382b2de7804","src/client.rs":"3f07a9a9d539cefe0517bc38052248424f6caaf2046924e757ca7540cd678ffb","src/error.rs":"d4d9a1f6fbe18b7f940a2c2684fa6184bf335b254eab7cc3cba41b16e4c9232b","src/lib.rs":"581c9aaabf92c59cf206387b6efe16d951bc85fbef57b7dda8d1a987184ba488","src/model.rs":"56408fbbe21516736455100e4cb0fd52484dbe6826b72cdcb35e184734fe96f9","src/operation.rs":"2d7413fcb6096085bae0012352a381b48fb4379bdabe6884147fed8b57041f05","src/operation/builder.rs":"e12363159bc736c97534a2f927915c3d8289391849435cc4f44e3d4412882d63","src/operation/cups.rs":"2fad56b3d645371c4efd497a6e5ef2c4df8a137a5cd4bdf01cd434d2a8383768","src/parser.rs":"c57718495120142fcbd89101fb03263b3ad1ec3a6caa043f05da17e8599294f5","src/payload.rs":"3ac50acd69238e913c547be58010aa563d205e8955043e6b0d686864d6536513","src/reader.rs":"f58af769e96781c5d715f6471a6f92de82442fab0244d3611edb052b7134b346","src/request.rs":"5337ca3f0d54f1e7af44fb3716d9c7379ec992e3d538b581fb007a502ba06386","src/util.rs":"a23e3ef1fa1f9293dbe1ddf599062c680ad7adb86b4edb230fd049ac95840894","src/value.rs":"f06d06520069fbb1376118e97bfd4fc6d696ea8d2bb6861d5329fffadadcb879"}}
\ No newline at end of file
diff --git a/crates/ipp/.cargo-checksum.json b/crates/ipp/.cargo-checksum.json
new file mode 100644
index 0000000..d50e148
--- /dev/null
+++ b/crates/ipp/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"83161f1835c51f1430a256d1de3fb4da13d78c9727a75f312c36b982775f1a73","README.md":"98d37d36dbb834b970f3cdfb71f9c81f68d900f99bda079de5bb4d021d4f8a9a","src/attribute.rs":"2c01ae2fa5a48b968b43622793a34974b8153544b1f7220743b14416085dab11","src/client.rs":"a3cebe2d7727b12054b91ea998b0c27612ced41e455010e59a084db4a2836142","src/error.rs":"aefe12457e30f4a7484c6d4bfa4b366a04c8525f30f6de9755ce9ebc220d8959","src/lib.rs":"d87167ee8c8d61091d98f6d25fc54873b8d3430f47a826cf0f50c854a21cf5ed","src/model.rs":"ac6fd2898c61811bc89e36412c2647677b88b65e44b73e7c4172dabc11033490","src/operation.rs":"f96e94228462cbf21dbe42e4e3790bdd28191a183b9b3361fce04259c22e8880","src/operation/builder.rs":"778de0432f1ef8bfaab368b371c2d2cde810e20a9c9b02049d2de1d26308100b","src/operation/cups.rs":"e6812e013e1a8819e428548125d5bbdbabe60bf2242f3b4b35598a4fc95aea88","src/parser.rs":"fc35f55ab4da85a6323c38e0b63c774a97e27784b028ece5ce5d99e27fbaae3a","src/payload.rs":"b9d2a0e02a0dd54ae2037c77f08b741798c32242a43b47bec17841bb4dfe9d45","src/reader.rs":"6cba49671f8bd13ac5f68b9adbec64260d5479ff9d10debeef13aa5fa8a24131","src/request.rs":"4ff4f28347e08725b3d465d07e2f8d92d1f1e59f2656fa71d52443c66ef485e3","src/util.rs":"cf0c5f9674b2120376dffda637b9aa0bbe33226e7eeb01e7cfd7fe5b1c93ccc1","src/value.rs":"2ac219754ff7541ca76805254345474d306dbf7a4c0f258ec99100008fc5bf4e"},"package":"0a74ba7383ea538b5c356323681ec5f4208eedf502e4e13886c82cf65af636ca"}
\ No newline at end of file
diff --git a/crates/ipp/Android.bp b/crates/ipp/Android.bp
new file mode 100644
index 0000000..a24c0cf
--- /dev/null
+++ b/crates/ipp/Android.bp
@@ -0,0 +1,83 @@
+// This file is generated by cargo_embargo.
+// Do not modify this file because the changes will be overridden on upgrade.
+
+package {
+ default_applicable_licenses: ["external_rust_crates_ipp_license"],
+ default_team: "trendy_team_android_rust",
+}
+
+license {
+ name: "external_rust_crates_ipp_license",
+ visibility: [":__subpackages__"],
+ license_kinds: ["SPDX-license-identifier-Apache-2.0"],
+ license_text: ["LICENSE"],
+}
+
+rust_test {
+ name: "ipp_test_src_lib",
+ host_supported: true,
+ crate_name: "ipp",
+ cargo_env_compat: true,
+ cargo_pkg_version: "5.2.0",
+ crate_root: "src/lib.rs",
+ test_suites: ["general-tests"],
+ auto_gen_config: true,
+ test_options: {
+ unit_test: true,
+ },
+ edition: "2021",
+ features: [
+ "async",
+ "futures-executor",
+ "futures-util",
+ ],
+ rustlibs: [
+ "libbytes",
+ "libfutures_executor",
+ "libfutures_util",
+ "libhttp",
+ "liblog_rust",
+ "libnum_traits",
+ "libthiserror",
+ "libtokio",
+ ],
+ proc_macros: [
+ "libenum_as_inner",
+ "libenum_primitive_derive",
+ ],
+}
+
+rust_library {
+ name: "libipp",
+ host_supported: true,
+ crate_name: "ipp",
+ cargo_env_compat: true,
+ cargo_pkg_version: "5.2.0",
+ crate_root: "src/lib.rs",
+ edition: "2021",
+ features: [
+ "async",
+ "futures-executor",
+ "futures-util",
+ ],
+ rustlibs: [
+ "libbytes",
+ "libfutures_executor",
+ "libfutures_util",
+ "libhttp",
+ "liblog_rust",
+ "libnum_traits",
+ "libthiserror",
+ ],
+ proc_macros: [
+ "libenum_as_inner",
+ "libenum_primitive_derive",
+ ],
+ apex_available: [
+ "//apex_available:platform",
+ "//apex_available:anyapex",
+ ],
+ product_available: true,
+ vendor_available: true,
+ min_sdk_version: "29",
+}
diff --git a/crates/ipp/Cargo.toml b/crates/ipp/Cargo.toml
new file mode 100644
index 0000000..ac3cb66
--- /dev/null
+++ b/crates/ipp/Cargo.toml
@@ -0,0 +1,170 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+name = "ipp"
+version = "5.2.0"
+authors = ["Dmitry Pankratov <dmitry@pankratov.net>"]
+build = false
+autolib = false
+autobins = false
+autoexamples = false
+autotests = false
+autobenches = false
+description = "Asynchronous IPP print protocol implementation"
+documentation = "https://docs.rs/ipp"
+readme = "README.md"
+keywords = [
+ "ipp",
+ "print",
+ "cups",
+ "printing",
+ "protocol",
+]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/ancwrd1/ipp.rs"
+
+[lib]
+name = "ipp"
+path = "src/lib.rs"
+
+[dependencies.base64]
+version = "0.22"
+optional = true
+
+[dependencies.bytes]
+version = "1"
+
+[dependencies.enum-as-inner]
+version = "0.6"
+
+[dependencies.enum-primitive-derive]
+version = "0.3"
+
+[dependencies.futures-executor]
+version = "0.3"
+optional = true
+
+[dependencies.futures-util]
+version = "0.3"
+features = ["io"]
+optional = true
+default-features = false
+
+[dependencies.http]
+version = "1"
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.native-tls]
+version = "0.2"
+optional = true
+
+[dependencies.num-traits]
+version = "0.2"
+
+[dependencies.once_cell]
+version = "1"
+optional = true
+
+[dependencies.reqwest]
+version = "0.12"
+features = ["stream"]
+optional = true
+default-features = false
+
+[dependencies.rustls]
+version = "0.23"
+features = [
+ "ring",
+ "log",
+ "tls12",
+ "std",
+]
+optional = true
+default-features = false
+
+[dependencies.rustls-native-certs]
+version = "0.8"
+optional = true
+
+[dependencies.serde]
+version = "1"
+features = ["derive"]
+optional = true
+
+[dependencies.thiserror]
+version = "2"
+
+[dependencies.tokio-util]
+version = "0.7"
+features = [
+ "io",
+ "compat",
+]
+optional = true
+
+[dependencies.ureq]
+version = "2"
+optional = true
+default-features = false
+
+[dev-dependencies.tokio]
+version = "1"
+features = [
+ "macros",
+ "rt-multi-thread",
+]
+
+[features]
+async = [
+ "futures-util",
+ "futures-executor",
+]
+async-client = [
+ "async",
+ "reqwest",
+ "tokio-util",
+ "base64",
+]
+async-client-rustls = [
+ "async-client",
+ "rustls",
+ "reqwest/rustls-tls-native-roots",
+]
+async-client-tls = [
+ "async-client",
+ "native-tls",
+ "reqwest/native-tls",
+]
+client = [
+ "ureq",
+ "base64",
+]
+client-rustls = [
+ "client",
+ "rustls",
+ "rustls-native-certs",
+ "once_cell",
+ "ureq/tls",
+]
+client-tls = [
+ "client",
+ "native-tls",
+ "ureq/native-tls",
+]
+default = ["async-client-tls"]
+serde = [
+ "dep:serde",
+ "bytes/serde",
+]
diff --git a/crates/ipp/LICENSE b/crates/ipp/LICENSE
new file mode 100644
index 0000000..f49a4e1
--- /dev/null
+++ b/crates/ipp/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/crates/ipp/METADATA b/crates/ipp/METADATA
new file mode 100644
index 0000000..8b6f16e
--- /dev/null
+++ b/crates/ipp/METADATA
@@ -0,0 +1,17 @@
+name: "ipp"
+description: "Asynchronous IPP print protocol implementation"
+third_party {
+ version: "5.2.0"
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2025
+ month: 3
+ day: 21
+ }
+ homepage: "https://crates.io/crates/ipp"
+ identifier {
+ type: "Archive"
+ value: "https://static.crates.io/crates/ipp/ipp-5.2.0.crate"
+ version: "5.2.0"
+ }
+}
diff --git a/crates/ipp/MODULE_LICENSE_APACHE2 b/crates/ipp/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/ipp/MODULE_LICENSE_APACHE2
diff --git a/crates/ipp/README.md b/crates/ipp/README.md
new file mode 100644
index 0000000..c28a79a
--- /dev/null
+++ b/crates/ipp/README.md
@@ -0,0 +1,58 @@
+# ipp.rs
+
+[](https://github.com/ancwrd1/ipp.rs/actions)
+[](https://crates.io/crates/ipp)
+[](https://opensource.org/licenses/MIT)
+[](https://opensource.org/licenses/Apache-2.0)
+[](https://docs.rs/ipp)
+
+IPP protocol implementation for Rust.
+This crate implements IPP protocol as defined in [RFC 8010](https://tools.ietf.org/html/rfc8010), [RFC 8011](https://tools.ietf.org/html/rfc8011).
+
+It supports both synchronous and asynchronous operations (requests and responses) which is controlled by the `async` feature flag.
+
+The following build-time features are supported:
+
+* `async` - enables asynchronous APIs.
+* `async-client` - enables asynchronous IPP client based on `reqwest` crate, implies `async` feature.
+* `client` - enables blocking IPP client based on `ureq` crate.
+* `async-client-tls` - enables asynchronous IPP client with TLS, using native-tls backend. Implies `async-client` feature.
+* `client-tls` - enables blocking IPP client with TLS, using native-tls backend. Implies `client` feature.
+* `async-client-rustls` - enables asynchronous IPP client with TLS, using rustls backend. Implies `async-client` feature.
+* `client-rustls` - enables blocking IPP client with TLS, using rustls backend. Implies `client` feature.
+
+By default, the following features are enabled: `async-client-tls`.
+Use `default-features=false` dependency option to disable them.
+
+[Documentation](https://docs.rs/ipp/latest/ipp/)
+
+Usage example for async client:
+
+```rust
+use ipp::prelude::*;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let uri: Uri = "http://localhost:631/printers/test-printer".parse()?;
+ let operation = IppOperationBuilder::get_printer_attributes(uri.clone()).build();
+ let client = AsyncIppClient::new(uri);
+ let resp = client.send(operation).await?;
+ if resp.header().status_code().is_success() {
+ let printer_attrs = resp
+ .attributes()
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap();
+ for (_, v) in printer_attrs.attributes() {
+ println!("{}: {}", v.name(), v.value());
+ }
+ }
+ Ok(())
+}
+```
+
+For more usage examples please check the [examples folder](https://github.com/ancwrd1/ipp.rs/tree/master/examples).
+
+## License
+
+Licensed under MIT or Apache license ([LICENSE-MIT](https://opensource.org/licenses/MIT) or [LICENSE-APACHE](https://opensource.org/licenses/Apache-2.0))
diff --git a/crates/ipp/TEST_MAPPING b/crates/ipp/TEST_MAPPING
new file mode 100644
index 0000000..35f7874
--- /dev/null
+++ b/crates/ipp/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "ipp_test_src_lib"
+ }
+ ]
+}
diff --git a/crates/ipp/cargo_embargo.json b/crates/ipp/cargo_embargo.json
new file mode 100644
index 0000000..daf2ad7
--- /dev/null
+++ b/crates/ipp/cargo_embargo.json
@@ -0,0 +1,6 @@
+{
+ "run_cargo": false,
+ "min_sdk_version": "29",
+ "tests": true,
+ "features": ["async"]
+}
diff --git a/crates/ipp/patches/LICENSE.patch b/crates/ipp/patches/LICENSE.patch
new file mode 100644
index 0000000..bc7920a
--- /dev/null
+++ b/crates/ipp/patches/LICENSE.patch
@@ -0,0 +1,207 @@
+diff --git a/LICENSE b/LICENSE
+index e69de29b..f49a4e16 100644
+--- a/LICENSE
++++ b/LICENSE
+@@ -0,0 +1,201 @@
++ Apache License
++ Version 2.0, January 2004
++ http://www.apache.org/licenses/
++
++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
++
++ 1. Definitions.
++
++ "License" shall mean the terms and conditions for use, reproduction,
++ and distribution as defined by Sections 1 through 9 of this document.
++
++ "Licensor" shall mean the copyright owner or entity authorized by
++ the copyright owner that is granting the License.
++
++ "Legal Entity" shall mean the union of the acting entity and all
++ other entities that control, are controlled by, or are under common
++ control with that entity. For the purposes of this definition,
++ "control" means (i) the power, direct or indirect, to cause the
++ direction or management of such entity, whether by contract or
++ otherwise, or (ii) ownership of fifty percent (50%) or more of the
++ outstanding shares, or (iii) beneficial ownership of such entity.
++
++ "You" (or "Your") shall mean an individual or Legal Entity
++ exercising permissions granted by this License.
++
++ "Source" form shall mean the preferred form for making modifications,
++ including but not limited to software source code, documentation
++ source, and configuration files.
++
++ "Object" form shall mean any form resulting from mechanical
++ transformation or translation of a Source form, including but
++ not limited to compiled object code, generated documentation,
++ and conversions to other media types.
++
++ "Work" shall mean the work of authorship, whether in Source or
++ Object form, made available under the License, as indicated by a
++ copyright notice that is included in or attached to the work
++ (an example is provided in the Appendix below).
++
++ "Derivative Works" shall mean any work, whether in Source or Object
++ form, that is based on (or derived from) the Work and for which the
++ editorial revisions, annotations, elaborations, or other modifications
++ represent, as a whole, an original work of authorship. For the purposes
++ of this License, Derivative Works shall not include works that remain
++ separable from, or merely link (or bind by name) to the interfaces of,
++ the Work and Derivative Works thereof.
++
++ "Contribution" shall mean any work of authorship, including
++ the original version of the Work and any modifications or additions
++ to that Work or Derivative Works thereof, that is intentionally
++ submitted to Licensor for inclusion in the Work by the copyright owner
++ or by an individual or Legal Entity authorized to submit on behalf of
++ the copyright owner. For the purposes of this definition, "submitted"
++ means any form of electronic, verbal, or written communication sent
++ to the Licensor or its representatives, including but not limited to
++ communication on electronic mailing lists, source code control systems,
++ and issue tracking systems that are managed by, or on behalf of, the
++ Licensor for the purpose of discussing and improving the Work, but
++ excluding communication that is conspicuously marked or otherwise
++ designated in writing by the copyright owner as "Not a Contribution."
++
++ "Contributor" shall mean Licensor and any individual or Legal Entity
++ on behalf of whom a Contribution has been received by Licensor and
++ subsequently incorporated within the Work.
++
++ 2. Grant of Copyright License. Subject to the terms and conditions of
++ this License, each Contributor hereby grants to You a perpetual,
++ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++ copyright license to reproduce, prepare Derivative Works of,
++ publicly display, publicly perform, sublicense, and distribute the
++ Work and such Derivative Works in Source or Object form.
++
++ 3. Grant of Patent License. Subject to the terms and conditions of
++ this License, each Contributor hereby grants to You a perpetual,
++ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
++ (except as stated in this section) patent license to make, have made,
++ use, offer to sell, sell, import, and otherwise transfer the Work,
++ where such license applies only to those patent claims licensable
++ by such Contributor that are necessarily infringed by their
++ Contribution(s) alone or by combination of their Contribution(s)
++ with the Work to which such Contribution(s) was submitted. If You
++ institute patent litigation against any entity (including a
++ cross-claim or counterclaim in a lawsuit) alleging that the Work
++ or a Contribution incorporated within the Work constitutes direct
++ or contributory patent infringement, then any patent licenses
++ granted to You under this License for that Work shall terminate
++ as of the date such litigation is filed.
++
++ 4. Redistribution. You may reproduce and distribute copies of the
++ Work or Derivative Works thereof in any medium, with or without
++ modifications, and in Source or Object form, provided that You
++ meet the following conditions:
++
++ (a) You must give any other recipients of the Work or
++ Derivative Works a copy of this License; and
++
++ (b) You must cause any modified files to carry prominent notices
++ stating that You changed the files; and
++
++ (c) You must retain, in the Source form of any Derivative Works
++ that You distribute, all copyright, patent, trademark, and
++ attribution notices from the Source form of the Work,
++ excluding those notices that do not pertain to any part of
++ the Derivative Works; and
++
++ (d) If the Work includes a "NOTICE" text file as part of its
++ distribution, then any Derivative Works that You distribute must
++ include a readable copy of the attribution notices contained
++ within such NOTICE file, excluding those notices that do not
++ pertain to any part of the Derivative Works, in at least one
++ of the following places: within a NOTICE text file distributed
++ as part of the Derivative Works; within the Source form or
++ documentation, if provided along with the Derivative Works; or,
++ within a display generated by the Derivative Works, if and
++ wherever such third-party notices normally appear. The contents
++ of the NOTICE file are for informational purposes only and
++ do not modify the License. You may add Your own attribution
++ notices within Derivative Works that You distribute, alongside
++ or as an addendum to the NOTICE text from the Work, provided
++ that such additional attribution notices cannot be construed
++ as modifying the License.
++
++ You may add Your own copyright statement to Your modifications and
++ may provide additional or different license terms and conditions
++ for use, reproduction, or distribution of Your modifications, or
++ for any such Derivative Works as a whole, provided Your use,
++ reproduction, and distribution of the Work otherwise complies with
++ the conditions stated in this License.
++
++ 5. Submission of Contributions. Unless You explicitly state otherwise,
++ any Contribution intentionally submitted for inclusion in the Work
++ by You to the Licensor shall be under the terms and conditions of
++ this License, without any additional terms or conditions.
++ Notwithstanding the above, nothing herein shall supersede or modify
++ the terms of any separate license agreement you may have executed
++ with Licensor regarding such Contributions.
++
++ 6. Trademarks. This License does not grant permission to use the trade
++ names, trademarks, service marks, or product names of the Licensor,
++ except as required for reasonable and customary use in describing the
++ origin of the Work and reproducing the content of the NOTICE file.
++
++ 7. Disclaimer of Warranty. Unless required by applicable law or
++ agreed to in writing, Licensor provides the Work (and each
++ Contributor provides its Contributions) on an "AS IS" BASIS,
++ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
++ implied, including, without limitation, any warranties or conditions
++ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
++ PARTICULAR PURPOSE. You are solely responsible for determining the
++ appropriateness of using or redistributing the Work and assume any
++ risks associated with Your exercise of permissions under this License.
++
++ 8. Limitation of Liability. In no event and under no legal theory,
++ whether in tort (including negligence), contract, or otherwise,
++ unless required by applicable law (such as deliberate and grossly
++ negligent acts) or agreed to in writing, shall any Contributor be
++ liable to You for damages, including any direct, indirect, special,
++ incidental, or consequential damages of any character arising as a
++ result of this License or out of the use or inability to use the
++ Work (including but not limited to damages for loss of goodwill,
++ work stoppage, computer failure or malfunction, or any and all
++ other commercial damages or losses), even if such Contributor
++ has been advised of the possibility of such damages.
++
++ 9. Accepting Warranty or Additional Liability. While redistributing
++ the Work or Derivative Works thereof, You may choose to offer,
++ and charge a fee for, acceptance of support, warranty, indemnity,
++ or other liability obligations and/or rights consistent with this
++ License. However, in accepting such obligations, You may act only
++ on Your own behalf and on Your sole responsibility, not on behalf
++ of any other Contributor, and only if You agree to indemnify,
++ defend, and hold each Contributor harmless for any liability
++ incurred by, or claims asserted against, such Contributor by reason
++ of your accepting any such warranty or additional liability.
++
++ END OF TERMS AND CONDITIONS
++
++ APPENDIX: How to apply the Apache License to your work.
++
++ To apply the Apache License to your work, attach the following
++ boilerplate notice, with the fields enclosed by brackets "[]"
++ replaced with your own identifying information. (Don't include
++ the brackets!) The text should be enclosed in the appropriate
++ comment syntax for the file format. We also recommend that a
++ file or class name and description of purpose be included on the
++ same "printed page" as the copyright notice for easier
++ identification within third-party archives.
++
++ Copyright [yyyy] [name of copyright owner]
++
++ Licensed under the Apache License, Version 2.0 (the "License");
++ you may not use this file except in compliance with the License.
++ You may obtain a copy of the License at
++
++ http://www.apache.org/licenses/LICENSE-2.0
++
++ Unless required by applicable law or agreed to in writing, software
++ distributed under the License is distributed on an "AS IS" BASIS,
++ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ See the License for the specific language governing permissions and
++ limitations under the License.
+\ No newline at end of file
diff --git a/crates/ipp/src/attribute.rs b/crates/ipp/src/attribute.rs
new file mode 100644
index 0000000..4266f70
--- /dev/null
+++ b/crates/ipp/src/attribute.rs
@@ -0,0 +1,289 @@
+//!
+//! Attribute-related structs
+//!
+use std::collections::HashMap;
+
+use bytes::{BufMut, Bytes, BytesMut};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use crate::{model::DelimiterTag, value::IppValue};
+
+fn is_header_attr(attr: &str) -> bool {
+ IppAttribute::HEADER_ATTRS.iter().any(|&at| at == attr)
+}
+
+/// `IppAttribute` represents an IPP attribute
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Clone, Debug)]
+pub struct IppAttribute {
+ /// Attribute name
+ name: String,
+ /// Attribute value
+ value: IppValue,
+}
+
+impl IppAttribute {
+ pub const ATTRIBUTES_CHARSET: &'static str = "attributes-charset";
+ pub const ATTRIBUTES_NATURAL_LANGUAGE: &'static str = "attributes-natural-language";
+ pub const CHARSET_CONFIGURED: &'static str = "charset-configured";
+ pub const CHARSET_SUPPORTED: &'static str = "charset-supported";
+ pub const COMPRESSION_SUPPORTED: &'static str = "compression-supported";
+ pub const DOCUMENT_FORMAT_DEFAULT: &'static str = "document-format-default";
+ pub const DOCUMENT_FORMAT_SUPPORTED: &'static str = "document-format-supported";
+ pub const DOCUMENT_FORMAT_PREFERRED: &'static str = "document-format-preferred";
+ pub const GENERATED_NATURAL_LANGUAGE_SUPPORTED: &'static str = "generated-natural-language-supported";
+ pub const IPP_VERSIONS_SUPPORTED: &'static str = "ipp-versions-supported";
+ pub const NATURAL_LANGUAGE_CONFIGURED: &'static str = "natural-language-configured";
+ pub const OPERATIONS_SUPPORTED: &'static str = "operations-supported";
+ pub const PDL_OVERRIDE_SUPPORTED: &'static str = "pdl-override-supported";
+ pub const PRINTER_IS_ACCEPTING_JOBS: &'static str = "printer-is-accepting-jobs";
+ pub const PRINTER_MAKE_AND_MODEL: &'static str = "printer-make-and-model";
+ pub const PRINTER_NAME: &'static str = "printer-name";
+ pub const PRINTER_STATE: &'static str = "printer-state";
+ pub const PRINTER_STATE_MESSAGE: &'static str = "printer-state-message";
+ pub const PRINTER_STATE_REASONS: &'static str = "printer-state-reasons";
+ pub const PRINTER_UP_TIME: &'static str = "printer-up-time";
+ pub const PRINTER_URI: &'static str = "printer-uri";
+ pub const PRINTER_URI_SUPPORTED: &'static str = "printer-uri-supported";
+ pub const PRINTER_FIRMWARE_NAME: &'static str = "printer-firmware-name";
+ pub const PRINTER_FIRMWARE_STRING_VERSION: &'static str = "printer-firmware-string-version";
+ pub const PRINTER_DEVICE_ID: &'static str = "printer-device-id";
+ pub const PRINTER_UUID: &'static str = "printer-uuid";
+ pub const QUEUED_JOB_COUNT: &'static str = "queued-job-count";
+ pub const URI_AUTHENTICATION_SUPPORTED: &'static str = "uri-authentication-supported";
+ pub const URI_SECURITY_SUPPORTED: &'static str = "uri-security-supported";
+ pub const JOB_ID: &'static str = "job-id";
+ pub const JOB_NAME: &'static str = "job-name";
+ pub const JOB_STATE: &'static str = "job-state";
+ pub const JOB_STATE_REASONS: &'static str = "job-state-reasons";
+ pub const JOB_URI: &'static str = "job-uri";
+ pub const LAST_DOCUMENT: &'static str = "last-document";
+ pub const REQUESTING_USER_NAME: &'static str = "requesting-user-name";
+ pub const STATUS_MESSAGE: &'static str = "status-message";
+ pub const REQUESTED_ATTRIBUTES: &'static str = "requested-attributes";
+ pub const SIDES_SUPPORTED: &'static str = "sides-supported";
+ pub const SIDES: &'static str = "sides";
+ pub const OUTPUT_MODE_SUPPORTED: &'static str = "output-mode-supported";
+ pub const COLOR_SUPPORTED: &'static str = "color-supported";
+ pub const PRINTER_INFO: &'static str = "printer-info";
+ pub const PRINTER_LOCATION: &'static str = "printer-location";
+ pub const PRINTER_MORE_INFO: &'static str = "printer-more-info";
+ pub const PRINTER_RESOLUTION_DEFAULT: &'static str = "printer-resolution-default";
+ pub const PRINTER_RESOLUTION_SUPPORTED: &'static str = "printer-resolution-supported";
+ pub const COPIES_SUPPORTED: &'static str = "copies-supported";
+ pub const COPIES_DEFAULT: &'static str = "copies-default";
+ pub const COPIES: &'static str = "copies";
+ pub const SIDES_DEFAULT: &'static str = "sides-default";
+ pub const PRINT_QUALITY_DEFAULT: &'static str = "print-quality-default";
+ pub const PRINT_QUALITY_SUPPORTED: &'static str = "print-quality-supported";
+ pub const FINISHINGS_DEFAULT: &'static str = "finishings-default";
+ pub const FINISHINGS_SUPPORTED: &'static str = "finishings-supported";
+ pub const OUTPUT_BIN_DEFAULT: &'static str = "output-bin-default";
+ pub const OUTPUT_BIN_SUPPORTED: &'static str = "output-bin-supported";
+ pub const ORIENTATION_REQUESTED_DEFAULT: &'static str = "orientation-requested-default";
+ pub const ORIENTATION_REQUESTED_SUPPORTED: &'static str = "orientation-requested-supported";
+ pub const MEDIA_DEFAULT: &'static str = "media-default";
+ pub const MEDIA_SUPPORTED: &'static str = "media-supported";
+ pub const MEDIA_COL_SUPPORTED: &'static str = "media-col-supported";
+ pub const MEDIA_TYPE_SUPPORTED: &'static str = "media-type-supported";
+ pub const PAGES_PER_MINUTE: &'static str = "pages-per-minute";
+ pub const COLOR_MODE_SUPPORTED: &'static str = "color-mode-supported";
+ pub const PRINT_COLOR_MODE_SUPPORTED: &'static str = "print-color-mode-supported";
+ pub const PRINT_COLOR_MODE_DEFAULT: &'static str = "print-color-mode-default";
+ pub const PRINT_COLOR_MODE: &'static str = "print-color-mode";
+ pub const MULTIPLE_DOCUMENT_HANDLING_SUPPORTED: &'static str = "multiple-document-handling-supported";
+ pub const MULTIPLE_DOCUMENT_HANDLING_DEFAULT: &'static str = "multiple-document-handling-default";
+ pub const MULTIPLE_DOCUMENT_HANDLING: &'static str = "multiple-document-handling";
+ pub const MEDIA_SOURCE_SUPPORTED: &'static str = "media-source-supported";
+ pub const MOPRIA_CERTIFIED: &'static str = "mopria-certified";
+ pub const ORIENTATION_REQUESTED: &'static str = "orientation-requested";
+ pub const OUTPUT_BIN: &'static str = "output-bin";
+ pub const PRINT_QUALITY: &'static str = "print-quality";
+ pub const PRINTER_RESOLUTION: &'static str = "printer-resolution";
+ pub const MEDIA_COL: &'static str = "media-col";
+ pub const FINISHINGS: &'static str = "finishings";
+
+ // Per section 4.1.4. Character Set and Natural Language Operation Attributes
+ // The "attributes-charset" and "attributes-natural-language" attributes MUST be the first two attributes
+ // in every IPP request and response, as part of the initial Operation Attributes group of the IPP message
+ // Per section 4.1.5 Operation targets
+ // o In the case where there is only one operation target attribute
+ // (i.e., either only the "printer-uri" attribute or only the
+ // "job-uri" attribute), that attribute MUST be the third attribute
+ // in the Operation Attributes group.
+ // o In the case where Job operations use two operation target
+ // attributes (i.e., the "printer-uri" and "job-id" attributes), the
+ // "printer-uri" attribute MUST be the third attribute and the
+ // "job-id" attribute MUST be the fourth attribute.
+ const HEADER_ATTRS: [&'static str; 3] = [
+ IppAttribute::ATTRIBUTES_CHARSET,
+ IppAttribute::ATTRIBUTES_NATURAL_LANGUAGE,
+ IppAttribute::PRINTER_URI,
+ ];
+
+ /// Create new instance of the attribute
+ ///
+ /// * `name` - Attribute name<br/>
+ /// * `value` - Attribute value<br/>
+ pub fn new<S>(name: S, value: IppValue) -> IppAttribute
+ where
+ S: AsRef<str>,
+ {
+ IppAttribute {
+ name: name.as_ref().to_owned(),
+ value,
+ }
+ }
+
+ /// Return attribute name
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ /// Return attribute value
+ pub fn value(&self) -> &IppValue {
+ &self.value
+ }
+
+ /// Consume this attribute and return the value
+ pub fn into_value(self) -> IppValue {
+ self.value
+ }
+
+ /// Write attribute to byte array
+ pub fn to_bytes(&self) -> Bytes {
+ let mut buffer = BytesMut::new();
+
+ buffer.put_u8(self.value.to_tag());
+ buffer.put_u16(self.name.len() as u16);
+ buffer.put_slice(self.name.as_bytes());
+ buffer.put(self.value.to_bytes());
+ buffer.freeze()
+ }
+}
+
+/// Attribute group
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Clone, Debug)]
+pub struct IppAttributeGroup {
+ tag: DelimiterTag,
+ attributes: HashMap<String, IppAttribute>,
+}
+
+impl IppAttributeGroup {
+ /// Create new attribute group of a given type
+ pub fn new(tag: DelimiterTag) -> IppAttributeGroup {
+ IppAttributeGroup {
+ tag,
+ attributes: HashMap::new(),
+ }
+ }
+
+ /// Return group type tag
+ pub fn tag(&self) -> DelimiterTag {
+ self.tag
+ }
+
+ /// Return read-only attributes
+ pub fn attributes(&self) -> &HashMap<String, IppAttribute> {
+ &self.attributes
+ }
+
+ /// Return mutable attributes
+ pub fn attributes_mut(&mut self) -> &mut HashMap<String, IppAttribute> {
+ &mut self.attributes
+ }
+
+ /// Consume this group and return mutable attributes
+ pub fn into_attributes(self) -> HashMap<String, IppAttribute> {
+ self.attributes
+ }
+}
+
+/// Attribute list
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Clone, Debug, Default)]
+pub struct IppAttributes {
+ groups: Vec<IppAttributeGroup>,
+}
+
+impl IppAttributes {
+ /// Create attribute list
+ pub fn new() -> IppAttributes {
+ IppAttributes { ..Default::default() }
+ }
+
+ /// Get all groups
+ pub fn groups(&self) -> &[IppAttributeGroup] {
+ &self.groups
+ }
+
+ /// Get all mutable groups
+ pub fn groups_mut(&mut self) -> &mut Vec<IppAttributeGroup> {
+ &mut self.groups
+ }
+
+ /// Consume this attribute list and return all attribute groups
+ pub fn into_groups(self) -> Vec<IppAttributeGroup> {
+ self.groups
+ }
+
+ /// Get a list of attribute groups matching a given delimiter tag
+ pub fn groups_of(&self, tag: DelimiterTag) -> impl Iterator<Item = &IppAttributeGroup> {
+ self.groups.iter().filter(move |g| g.tag == tag)
+ }
+
+ /// Add attribute to a given group
+ pub fn add(&mut self, tag: DelimiterTag, attribute: IppAttribute) {
+ let group = self.groups_mut().iter_mut().find(|g| g.tag() == tag);
+ if let Some(group) = group {
+ group.attributes_mut().insert(attribute.name().to_owned(), attribute);
+ } else {
+ let mut new_group = IppAttributeGroup::new(tag);
+ new_group
+ .attributes_mut()
+ .insert(attribute.name().to_owned(), attribute);
+ self.groups_mut().push(new_group);
+ }
+ }
+
+ /// Write attribute list to byte array
+ pub fn to_bytes(&self) -> Bytes {
+ let mut buffer = BytesMut::new();
+
+ // put the required attributes first as described in section 4.1.4 of RFC8011
+ buffer.put_u8(DelimiterTag::OperationAttributes as u8);
+
+ if let Some(group) = self.groups_of(DelimiterTag::OperationAttributes).next() {
+ for hdr in &IppAttribute::HEADER_ATTRS {
+ if let Some(attr) = group.attributes().get(*hdr) {
+ buffer.put(attr.to_bytes());
+ }
+ }
+
+ // now the other operation attributes
+ for attr in group.attributes().values() {
+ if !is_header_attr(attr.name()) {
+ buffer.put(attr.to_bytes());
+ }
+ }
+ }
+
+ // now the rest
+ for group in self
+ .groups()
+ .iter()
+ .filter(|group| group.tag() != DelimiterTag::OperationAttributes)
+ {
+ buffer.put_u8(group.tag() as u8);
+
+ for attr in group.attributes().values() {
+ buffer.put(attr.to_bytes());
+ }
+ }
+ buffer.put_u8(DelimiterTag::EndOfAttributes as u8);
+
+ buffer.freeze()
+ }
+}
diff --git a/crates/ipp/src/client.rs b/crates/ipp/src/client.rs
new file mode 100644
index 0000000..b9fabc3
--- /dev/null
+++ b/crates/ipp/src/client.rs
@@ -0,0 +1,412 @@
+//!
+//! IPP client
+//!
+use std::{collections::BTreeMap, marker::PhantomData, time::Duration};
+
+use base64::Engine;
+use http::Uri;
+
+const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
+
+fn ipp_uri_to_string(uri: &Uri) -> String {
+ let (scheme, default_port) = match uri.scheme_str() {
+ Some("ipps") => ("https", 443),
+ Some("ipp") => ("http", 631),
+ _ => return uri.to_string(),
+ };
+
+ let authority = match uri.authority() {
+ Some(authority) => {
+ if authority.port_u16().is_some() {
+ authority.to_string()
+ } else {
+ format!("{}:{}", authority, default_port)
+ }
+ }
+ None => return uri.to_string(),
+ };
+
+ let path_and_query = uri.path_and_query().map(|p| p.as_str()).unwrap_or_default();
+
+ format!("{}://{}{}", scheme, authority, path_and_query)
+}
+
+/// Builder to create IPP client
+pub struct IppClientBuilder<T> {
+ uri: Uri,
+ ignore_tls_errors: bool,
+ request_timeout: Option<Duration>,
+ headers: BTreeMap<String, String>,
+ ca_certs: Vec<Vec<u8>>,
+ _phantom_data: PhantomData<T>,
+}
+
+impl<T> IppClientBuilder<T> {
+ fn new(uri: Uri) -> Self {
+ IppClientBuilder {
+ uri,
+ ignore_tls_errors: false,
+ request_timeout: None,
+ headers: BTreeMap::new(),
+ ca_certs: Vec::new(),
+ _phantom_data: PhantomData,
+ }
+ }
+
+ /// Enable or disable ignoring of TLS handshake errors. Default is false.
+ pub fn ignore_tls_errors(mut self, flag: bool) -> Self {
+ self.ignore_tls_errors = flag;
+ self
+ }
+
+ /// Add custom root certificate in PEM or DER format.
+ pub fn ca_cert<D: AsRef<[u8]>>(mut self, data: D) -> Self {
+ self.ca_certs.push(data.as_ref().to_owned());
+ self
+ }
+
+ /// Set network request timeout. Default is no timeout.
+ pub fn request_timeout(mut self, duration: Duration) -> Self {
+ self.request_timeout = Some(duration);
+ self
+ }
+
+ /// Add custom HTTP header
+ pub fn http_header<K, V>(mut self, key: K, value: V) -> Self
+ where
+ K: AsRef<str>,
+ V: AsRef<str>,
+ {
+ self.headers.insert(key.as_ref().to_owned(), value.as_ref().to_owned());
+ self
+ }
+
+ /// Add basic auth header (RFC 7617)
+ pub fn basic_auth<U, P>(mut self, username: U, password: P) -> Self
+ where
+ U: AsRef<str>,
+ P: AsRef<str>,
+ {
+ let authz =
+ base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", username.as_ref(), password.as_ref()));
+ self.headers
+ .insert("authorization".to_owned(), format!("Basic {authz}"));
+ self
+ }
+}
+
+#[cfg(feature = "async-client")]
+impl IppClientBuilder<non_blocking::AsyncIppClient> {
+ /// Build the async client
+ pub fn build(self) -> non_blocking::AsyncIppClient {
+ non_blocking::AsyncIppClient(self)
+ }
+}
+
+#[cfg(feature = "client")]
+impl IppClientBuilder<blocking::IppClient> {
+ /// Build the blocking client
+ pub fn build(self) -> blocking::IppClient {
+ blocking::IppClient(self)
+ }
+}
+
+#[cfg(feature = "async-client")]
+pub mod non_blocking {
+ use std::io;
+
+ use futures_util::{io::BufReader, stream::TryStreamExt};
+ use http::Uri;
+ use reqwest::{Body, ClientBuilder};
+ use tokio_util::compat::FuturesAsyncReadCompatExt;
+
+ use crate::{error::IppError, parser::AsyncIppParser, request::IppRequestResponse};
+
+ use super::{ipp_uri_to_string, IppClientBuilder, CONNECT_TIMEOUT};
+
+ const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), ";reqwest");
+
+ /// Asynchronous IPP client.
+ ///
+ /// IPP client is responsible for sending requests to IPP server.
+ pub struct AsyncIppClient(pub(super) IppClientBuilder<Self>);
+
+ impl AsyncIppClient {
+ /// Create IPP client with default options
+ pub fn new(uri: Uri) -> Self {
+ AsyncIppClient(AsyncIppClient::builder(uri))
+ }
+
+ /// Create IPP client builder for setting extra options
+ pub fn builder(uri: Uri) -> IppClientBuilder<Self> {
+ IppClientBuilder::new(uri)
+ }
+
+ /// Return client URI
+ pub fn uri(&self) -> &Uri {
+ &self.0.uri
+ }
+
+ /// Send IPP request to the server
+ pub async fn send<R>(&self, request: R) -> Result<IppRequestResponse, IppError>
+ where
+ R: Into<IppRequestResponse>,
+ {
+ let mut builder = ClientBuilder::new().connect_timeout(CONNECT_TIMEOUT);
+
+ if let Some(timeout) = self.0.request_timeout {
+ builder = builder.timeout(timeout);
+ }
+
+ #[cfg(any(feature = "async-client-tls", feature = "async-client-rustls"))]
+ {
+ if self.0.ignore_tls_errors {
+ builder = builder
+ .danger_accept_invalid_hostnames(true)
+ .danger_accept_invalid_certs(true);
+ }
+ for data in &self.0.ca_certs {
+ let cert =
+ reqwest::Certificate::from_pem(data).or_else(|_| reqwest::Certificate::from_der(data))?;
+ builder = builder.add_root_certificate(cert);
+ }
+ }
+
+ #[cfg(feature = "async-client-rustls")]
+ {
+ builder = builder.use_rustls_tls();
+ }
+
+ let mut req_builder = builder
+ .user_agent(USER_AGENT)
+ .build()?
+ .post(ipp_uri_to_string(&self.0.uri));
+
+ for (k, v) in &self.0.headers {
+ req_builder = req_builder.header(k, v);
+ }
+
+ let response = req_builder
+ .header("content-type", "application/ipp")
+ .body(Body::wrap_stream(tokio_util::io::ReaderStream::new(
+ request.into().into_async_read().compat(),
+ )))
+ .send()
+ .await?;
+
+ if response.status().is_success() {
+ let parser = AsyncIppParser::new(BufReader::new(
+ response
+ .bytes_stream()
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
+ .into_async_read(),
+ ));
+ parser.parse().await.map_err(IppError::from)
+ } else {
+ Err(IppError::RequestError(response.status().as_u16()))
+ }
+ }
+ }
+}
+
+#[cfg(feature = "client")]
+pub mod blocking {
+ use http::Uri;
+ use ureq::AgentBuilder;
+
+ use crate::{error::IppError, parser::IppParser, reader::IppReader, request::IppRequestResponse};
+
+ use super::{ipp_uri_to_string, IppClientBuilder, CONNECT_TIMEOUT};
+
+ const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), ";ureq");
+
+ /// Blocking IPP client.
+ ///
+ /// IPP client is responsible for sending requests to IPP server.
+ pub struct IppClient(pub(super) IppClientBuilder<Self>);
+
+ impl IppClient {
+ /// Create IPP client with default options
+ pub fn new(uri: Uri) -> Self {
+ IppClient(IppClient::builder(uri))
+ }
+
+ /// Create IPP client builder for setting extra options
+ pub fn builder(uri: Uri) -> IppClientBuilder<Self> {
+ IppClientBuilder::new(uri)
+ }
+
+ /// Return client URI
+ pub fn uri(&self) -> &Uri {
+ &self.0.uri
+ }
+
+ /// Send IPP request to the server
+ pub fn send<R>(&self, request: R) -> Result<IppRequestResponse, IppError>
+ where
+ R: Into<IppRequestResponse>,
+ {
+ let mut builder = AgentBuilder::new().timeout_connect(CONNECT_TIMEOUT);
+
+ if let Some(timeout) = self.0.request_timeout {
+ builder = builder.timeout(timeout);
+ }
+
+ #[cfg(feature = "client-tls")]
+ {
+ let mut tls_builder = native_tls::TlsConnector::builder();
+
+ tls_builder
+ .danger_accept_invalid_hostnames(self.0.ignore_tls_errors)
+ .danger_accept_invalid_certs(self.0.ignore_tls_errors);
+
+ for data in &self.0.ca_certs {
+ let cert =
+ native_tls::Certificate::from_pem(data).or_else(|_| native_tls::Certificate::from_der(data))?;
+ tls_builder.add_root_certificate(cert);
+ }
+
+ let tls_connector = tls_builder.build()?;
+ builder = builder.tls_connector(std::sync::Arc::new(tls_connector));
+ }
+
+ #[cfg(feature = "client-rustls")]
+ {
+ use once_cell::sync::Lazy;
+ use rustls::pki_types::pem::PemObject;
+ use rustls_native_certs::{load_native_certs, CertificateResult};
+
+ static ROOTS: Lazy<CertificateResult> = Lazy::new(load_native_certs);
+
+ let mut root_store = rustls::RootCertStore::empty();
+ root_store.add_parsable_certificates(ROOTS.certs.clone());
+
+ for data in &self.0.ca_certs {
+ let cert = rustls::pki_types::CertificateDer::<'static>::from_pem_slice(data)
+ .unwrap_or_else(|_| rustls::pki_types::CertificateDer::from_slice(data));
+ root_store.add(cert)?;
+ }
+
+ let secure_config = rustls::ClientConfig::builder()
+ .with_root_certificates(root_store)
+ .with_no_client_auth();
+
+ let config = if self.0.ignore_tls_errors {
+ rustls::ClientConfig::builder()
+ .dangerous()
+ .with_custom_certificate_verifier(std::sync::Arc::new(verifiers::NoVerifier(
+ secure_config.crypto_provider().clone(),
+ )))
+ .with_no_client_auth()
+ } else {
+ secure_config
+ };
+
+ builder = builder.tls_config(std::sync::Arc::new(config));
+ }
+
+ let agent = builder.user_agent(USER_AGENT).build();
+
+ let mut req = agent
+ .post(&ipp_uri_to_string(&self.0.uri))
+ .set("content-type", "application/ipp");
+
+ for (k, v) in &self.0.headers {
+ req = req.set(k, v);
+ }
+
+ let response = req.send(request.into().into_read())?;
+ let reader = response.into_reader();
+ let parser = IppParser::new(IppReader::new(reader));
+
+ parser.parse().map_err(IppError::from)
+ }
+ }
+
+ #[cfg(feature = "client-rustls")]
+ mod verifiers {
+ use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
+ use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
+ use rustls::{crypto::CryptoProvider, DigitallySignedStruct, Error, SignatureScheme};
+ use std::sync::Arc;
+
+ #[derive(Debug)]
+ pub struct NoVerifier(pub Arc<CryptoProvider>);
+
+ impl ServerCertVerifier for NoVerifier {
+ fn verify_server_cert(
+ &self,
+ _end_entity: &CertificateDer,
+ _intermediates: &[CertificateDer],
+ _server_name: &ServerName,
+ _ocsp_response: &[u8],
+ _now: UnixTime,
+ ) -> Result<ServerCertVerified, Error> {
+ Ok(ServerCertVerified::assertion())
+ }
+
+ fn verify_tls12_signature(
+ &self,
+ _message: &[u8],
+ _cert: &CertificateDer,
+ _dss: &DigitallySignedStruct,
+ ) -> Result<HandshakeSignatureValid, Error> {
+ Ok(HandshakeSignatureValid::assertion())
+ }
+
+ fn verify_tls13_signature(
+ &self,
+ _message: &[u8],
+ _cert: &CertificateDer,
+ _dss: &DigitallySignedStruct,
+ ) -> Result<HandshakeSignatureValid, Error> {
+ Ok(HandshakeSignatureValid::assertion())
+ }
+
+ fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
+ self.0.signature_verification_algorithms.supported_schemes()
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::client::ipp_uri_to_string;
+ use http::Uri;
+
+ #[test]
+ fn test_ipp_uri_no_port() {
+ let uri = "ipp://user:pass@host/path?query=1234".parse::<Uri>().unwrap();
+ let http_uri = ipp_uri_to_string(&uri);
+ assert_eq!(http_uri, "http://user:pass@host:631/path?query=1234");
+ }
+
+ #[test]
+ fn test_ipp_uri_with_port() {
+ let uri = "ipp://user:pass@host:1000".parse::<Uri>().unwrap();
+ let http_uri = ipp_uri_to_string(&uri);
+ assert_eq!(http_uri, "http://user:pass@host:1000/");
+ }
+
+ #[test]
+ fn test_ipps_uri_no_port() {
+ let uri = "ipps://host".parse::<Uri>().unwrap();
+ let http_uri = ipp_uri_to_string(&uri);
+ assert_eq!(http_uri, "https://host:443/");
+ }
+
+ #[test]
+ fn test_ipps_uri_with_port() {
+ let uri = "ipps://host:8443".parse::<Uri>().unwrap();
+ let http_uri = ipp_uri_to_string(&uri);
+ assert_eq!(http_uri, "https://host:8443/");
+ }
+
+ #[test]
+ fn test_http_uri_no_change() {
+ let uri = "http://somehost".parse::<Uri>().unwrap();
+ let http_uri = ipp_uri_to_string(&uri);
+ assert_eq!(http_uri, uri.to_string());
+ }
+}
diff --git a/crates/ipp/src/error.rs b/crates/ipp/src/error.rs
new file mode 100644
index 0000000..071d624
--- /dev/null
+++ b/crates/ipp/src/error.rs
@@ -0,0 +1,68 @@
+//!
+//! IPP error
+//!
+use std::io;
+
+use http::uri::InvalidUri;
+
+use crate::{model::StatusCode, parser::IppParseError};
+
+/// IPP error
+#[allow(clippy::large_enum_variant)]
+#[derive(Debug, thiserror::Error)]
+pub enum IppError {
+ #[error(transparent)]
+ /// HTTP protocol error
+ HttpError(#[from] http::Error),
+
+ #[error(transparent)]
+ #[cfg(feature = "async-client")]
+ /// Client error
+ AsyncClientError(#[from] reqwest::Error),
+
+ #[error("HTTP request error: {0}")]
+ /// HTTP request error
+ RequestError(u16),
+
+ #[error(transparent)]
+ /// Network or file I/O error
+ IoError(#[from] io::Error),
+
+ #[error("IPP status error: {0}")]
+ /// IPP status error
+ StatusError(StatusCode),
+
+ #[error("Printer not ready")]
+ PrinterNotReady,
+
+ #[error(transparent)]
+ /// Parsing error
+ ParseError(#[from] IppParseError),
+
+ #[error("Missing attribute in response")]
+ /// Missing attribute in response
+ MissingAttribute,
+
+ #[error("Invalid attribute type")]
+ /// Invalid attribute type
+ InvalidAttributeType,
+
+ #[error(transparent)]
+ /// Invalid URI
+ InvalidUri(#[from] InvalidUri),
+
+ #[error(transparent)]
+ #[cfg(feature = "client")]
+ /// Client error
+ ClientError(#[from] ureq::Error),
+
+ #[error(transparent)]
+ #[cfg(any(feature = "async-client-tls", feature = "client-tls"))]
+ /// TLS error
+ TlsError(#[from] native_tls::Error),
+
+ #[error(transparent)]
+ #[cfg(feature = "client-rustls")]
+ /// TLS error
+ RustlsError(#[from] rustls::Error),
+}
diff --git a/crates/ipp/src/lib.rs b/crates/ipp/src/lib.rs
new file mode 100644
index 0000000..911cecf
--- /dev/null
+++ b/crates/ipp/src/lib.rs
@@ -0,0 +1,159 @@
+//!
+//! IPP print protocol implementation for Rust. This crate can be used in several ways:
+//! * using the low-level request/response API and building the requests manually.
+//! * using the higher-level operations API with builders. Currently only a subset of all IPP operations is supported.
+//! * using the built-in IPP client.
+//! * using any third-party HTTP client and send the serialized request manually.
+//!
+//! This crate supports both synchronous and asynchronous operations. The following feature flags are supported:
+//! * `async` - enables asynchronous APIs.
+//! * `async-client` - enables asynchronous IPP client based on `reqwest` crate, implies `async` feature.
+//! * `client` - enables blocking IPP client based on `ureq` crate.
+//! * `async-client-tls` - enables asynchronous IPP client with TLS, using native-tls backend. Implies `async-client` feature.
+//! * `client-tls` - enables blocking IPP client with TLS, using native-tls backend. Implies `client` feature.
+//! * `async-client-rustls` - enables asynchronous IPP client with TLS, using rustls backend. Implies `async-client` feature.
+//! * `client-rustls` - enables blocking IPP client with TLS, using rustls backend. Implies `client` feature.
+//!
+//! By default, the following feature is enabled: `async-client-tls`.
+//!
+//! Implementation notes:
+//! * all RFC IPP values are supported including arrays and collections, for both de- and serialization.
+//! * this crate is also suitable for building IPP servers, however the example is not provided yet.
+//!
+//! Usage examples:
+//!
+//!```rust,no_run
+//! // using low-level async API
+//! use ipp::prelude::*;
+//!
+//! #[tokio::main]
+//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
+//! let uri: Uri = "http://localhost:631/printers/test-printer".parse()?;
+//! let req = IppRequestResponse::new(
+//! IppVersion::v1_1(),
+//! Operation::GetPrinterAttributes,
+//! Some(uri.clone())
+//! );
+//! let client = AsyncIppClient::new(uri);
+//! let resp = client.send(req).await?;
+//! if resp.header().status_code().is_success() {
+//! println!("{:?}", resp.attributes());
+//! }
+//! Ok(())
+//! }
+//!```
+//!```rust,no_run
+//! // using blocking operations API
+//! use ipp::prelude::*;
+//!
+//! fn main() -> Result<(), Box<dyn std::error::Error>> {
+//! let uri: Uri = "http://localhost:631/printers/test-printer".parse()?;
+//! let operation = IppOperationBuilder::get_printer_attributes(uri.clone()).build();
+//! let client = IppClient::new(uri);
+//! let resp = client.send(operation)?;
+//! if resp.header().status_code().is_success() {
+//! println!("{:?}", resp.attributes());
+//! }
+//! Ok(())
+//! }
+//!```
+
+#![allow(clippy::result_large_err)]
+
+use bytes::{BufMut, Bytes, BytesMut};
+use num_traits::FromPrimitive;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use crate::model::{IppVersion, StatusCode};
+
+pub mod attribute;
+#[cfg(any(feature = "client", feature = "async-client"))]
+pub mod client;
+pub mod error;
+pub mod model;
+pub mod operation;
+pub mod parser;
+pub mod payload;
+pub mod reader;
+pub mod request;
+pub mod util;
+pub mod value;
+
+pub mod prelude {
+ //!
+ //! Common imports
+ //!
+ pub use http::Uri;
+ pub use num_traits::FromPrimitive as _;
+
+ pub use crate::{
+ attribute::{IppAttribute, IppAttributeGroup, IppAttributes},
+ model::*,
+ operation::builder::IppOperationBuilder,
+ payload::IppPayload,
+ request::IppRequestResponse,
+ value::IppValue,
+ };
+
+ pub use super::error::IppError;
+
+ #[cfg(feature = "async-client")]
+ pub use super::client::non_blocking::AsyncIppClient;
+
+ #[cfg(feature = "client")]
+ pub use super::client::blocking::IppClient;
+
+ pub use super::IppHeader;
+}
+
+/// IPP request and response header
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Clone, Debug)]
+pub struct IppHeader {
+ /// IPP protocol version
+ pub version: IppVersion,
+ /// Operation tag for requests, status for responses
+ pub operation_or_status: u16,
+ /// ID of the request
+ pub request_id: u32,
+}
+
+impl IppHeader {
+ /// Create IPP header
+ pub fn new(version: IppVersion, operation_or_status: u16, request_id: u32) -> IppHeader {
+ IppHeader {
+ version,
+ operation_or_status,
+ request_id,
+ }
+ }
+
+ /// Write header to a given writer
+ pub fn to_bytes(&self) -> Bytes {
+ let mut buffer = BytesMut::new();
+ buffer.put_u16(self.version.0);
+ buffer.put_u16(self.operation_or_status);
+ buffer.put_u32(self.request_id);
+
+ buffer.freeze()
+ }
+
+ /// Decode and get IPP status code from the header
+ pub fn status_code(&self) -> StatusCode {
+ StatusCode::from_u16(self.operation_or_status).unwrap_or(StatusCode::UnknownStatusCode)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_header_to_bytes() {
+ let header = IppHeader::new(IppVersion::v2_1(), 0x1234, 0xaa55_aa55);
+ let buf = header.to_bytes();
+ assert_eq!(buf, vec![0x02, 0x01, 0x12, 0x34, 0xaa, 0x55, 0xaa, 0x55]);
+ }
+}
diff --git a/crates/ipp/src/model.rs b/crates/ipp/src/model.rs
new file mode 100644
index 0000000..ef590fe
--- /dev/null
+++ b/crates/ipp/src/model.rs
@@ -0,0 +1,285 @@
+//!
+//! Base IPP definitions and tags
+//!
+use std::fmt;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use enum_primitive_derive::Primitive;
+
+/// IPP protocol version
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct IppVersion(pub u16);
+
+impl IppVersion {
+ pub fn v1_0() -> Self {
+ IppVersion(0x0100)
+ }
+ pub fn v1_1() -> Self {
+ IppVersion(0x0101)
+ }
+ pub fn v2_0() -> Self {
+ IppVersion(0x0200)
+ }
+ pub fn v2_1() -> Self {
+ IppVersion(0x0201)
+ }
+ pub fn v2_2() -> Self {
+ IppVersion(0x0202)
+ }
+}
+
+/// IPP operation constants
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+#[allow(clippy::upper_case_acronyms)]
+pub enum Operation {
+ PrintJob = 0x0002,
+ PrintUri = 0x0003,
+ ValidateJob = 0x0004,
+ CreateJob = 0x0005,
+ SendDocument = 0x0006,
+ SendUri = 0x0007,
+ CancelJob = 0x0008,
+ GetJobAttributes = 0x0009,
+ GetJobs = 0x000A,
+ GetPrinterAttributes = 0x000B,
+ HoldJob = 0x000C,
+ ReleaseJob = 0x000D,
+ RestartJob = 0x000E,
+ PausePrinter = 0x0010,
+ ResumePrinter = 0x0011,
+ PurgeJobs = 0x0012,
+
+ CupsGetDefault = 0x4001,
+ CupsGetPrinters = 0x4002,
+ CupsAddModifyPrinter = 0x4003,
+ CupsDeletePrinter = 0x4004,
+ CupsGetClasses = 0x4005,
+ CupsAddModifyClass = 0x4006,
+ CupsDeleteClass = 0x4007,
+ CupsAcceptJobs = 0x4008,
+ CupsRejectJobs = 0x4009,
+ CupsSetDefault = 0x400A,
+ CupsGetDevices = 0x400B,
+ CupsGetPPDs = 0x400C,
+ CupsMoveJob = 0x400D,
+ CupsAuthenticateJob = 0x400E,
+ CupsGetPPD = 0x400F,
+ CupsGetDocument = 0x4027,
+ CupsCreateLocalPrinter = 0x4028,
+}
+
+/// printer-state constants
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+pub enum PrinterState {
+ Idle = 3,
+ Processing = 4,
+ Stopped = 5,
+}
+
+/// paper orientation constants
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+pub enum Orientation {
+ Portrait = 3,
+ Landscape = 4,
+ ReverseLandscape = 5,
+ ReversePortrait = 6,
+}
+
+/// print-quality constants
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+pub enum PrintQuality {
+ Draft = 3,
+ Normal = 4,
+ High = 5,
+}
+
+/// finishings constants
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+pub enum Finishings {
+ None = 3,
+ Staple = 4,
+ Punch = 5,
+ Cover = 6,
+ Bind = 7,
+ SaddleStitch = 8,
+ EdgeStitch = 9,
+ StapleTopLeft = 20,
+ StapleBottomLeft = 21,
+ StapleTopRight = 22,
+ StapleBottomRight = 23,
+ EdgeStitchLeft = 24,
+ EdgeStitchTop = 25,
+ EdgeStitchRight = 26,
+ EdgeStitchBottom = 27,
+ StapleDualLeft = 28,
+ StapleDualTop = 29,
+ StapleDualRight = 30,
+ StapleDualBottom = 31,
+ StapleTripleLeft = 32,
+ StapleTripleTop = 33,
+ StapleTripleRight = 34,
+ StapleTripleBottom = 35,
+ PunchTopLeft = 70,
+ PunchBottomLeft = 71,
+ PunchTopRight = 72,
+ PunchBottomRight = 73,
+ PunchDualLeft = 74,
+ PunchDualTop = 75,
+ PunchDualRight = 76,
+ PunchDualBottom = 77,
+ PunchTripleLeft = 78,
+ PunchTripleTop = 79,
+ PunchTripleRight = 80,
+ PunchTripleBottom = 81,
+ PunchQuadLeft = 82,
+ PunchQuadTop = 83,
+ PunchQuadRight = 84,
+ PunchQuadBottom = 85,
+}
+
+/// job-state constants
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+pub enum JobState {
+ Pending = 3,
+ PendingHeld = 4,
+ Processing = 5,
+ ProcessingStopped = 6,
+ Canceled = 7,
+ Aborted = 8,
+ Completed = 9,
+}
+
+/// group delimiter tags
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Primitive, Debug, Copy, Clone, PartialEq, Hash, Eq)]
+pub enum DelimiterTag {
+ OperationAttributes = 0x01,
+ JobAttributes = 0x02,
+ EndOfAttributes = 0x03,
+ PrinterAttributes = 0x04,
+ UnsupportedAttributes = 0x05,
+}
+
+/// IPP value tags
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+pub enum ValueTag {
+ Unsupported = 0x10,
+ Unknown = 0x12,
+ NoValue = 0x13,
+ Integer = 0x21,
+ Boolean = 0x22,
+ Enum = 0x23,
+ OctetStringUnspecified = 0x30,
+ DateTime = 0x31,
+ Resolution = 0x32,
+ RangeOfInteger = 0x33,
+ BegCollection = 0x34,
+ TextWithLanguage = 0x35,
+ NameWithLanguage = 0x36,
+ EndCollection = 0x37,
+ TextWithoutLanguage = 0x41,
+ NameWithoutLanguage = 0x42,
+ Keyword = 0x44,
+ Uri = 0x45,
+ UriScheme = 0x46,
+ Charset = 0x47,
+ NaturalLanguage = 0x48,
+ MimeMediaType = 0x49,
+ MemberAttrName = 0x4a,
+}
+
+/// IPP status codes
+#[derive(Primitive, Debug, Copy, Clone, Eq, PartialEq)]
+pub enum StatusCode {
+ SuccessfulOk = 0x0000,
+ SuccessfulOkIgnoredOrSubstitutedAttributes = 0x0001,
+ SuccessfulOkConflictingAttributes = 0x0002,
+ ClientErrorBadRequest = 0x0400,
+ ClientErrorForbidden = 0x0401,
+ ClientErrorNotAuthenticated = 0x0402,
+ ClientErrorNotAuthorized = 0x0403,
+ ClientErrorNotPossible = 0x0404,
+ ClientErrorTimeout = 0x0405,
+ ClientErrorNotFound = 0x0406,
+ ClientErrorGone = 0x0407,
+ ClientErrorRequestEntityTooLong = 0x0408,
+ ClientErrorRequestValueTooLong = 0x0409,
+ ClientErrorDocumentFormatNotSupported = 0x040A,
+ ClientErrorAttributesOrValuesNotSupported = 0x040B,
+ ClientErrorUriSchemeNotSupported = 0x040C,
+ ClientErrorCharsetNotSupported = 0x040D,
+ ClientErrorConflictingAttributes = 0x040E,
+ ClientErrorCompressionNotSupported = 0x040F,
+ ClientErrorCompressionError = 0x0410,
+ ClientErrorDocumentFormatError = 0x0411,
+ ClientErrorDocumentAccessError = 0x0412,
+ ServerErrorInternalError = 0x0500,
+ ServerErrorOperationNotSupported = 0x0501,
+ ServerErrorServiceUnavailable = 0x0502,
+ ServerErrorVersionNotSupported = 0x0503,
+ ServerErrorDeviceError = 0x0504,
+ ServerErrorTemporaryError = 0x0505,
+ ServerErrorNotAcceptingJobs = 0x0506,
+ ServerErrorBusy = 0x0507,
+ ServerErrorJobCanceled = 0x0508,
+ ServerErrorMultipleDocumentJobsNotSupported = 0x0509,
+ UnknownStatusCode = 0xffff,
+}
+
+impl StatusCode {
+ pub fn is_success(&self) -> bool {
+ matches!(
+ self,
+ StatusCode::SuccessfulOk
+ | StatusCode::SuccessfulOkIgnoredOrSubstitutedAttributes
+ | StatusCode::SuccessfulOkConflictingAttributes
+ )
+ }
+}
+
+impl fmt::Display for StatusCode {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ StatusCode::SuccessfulOk => write!(f, "No error"),
+ StatusCode::SuccessfulOkIgnoredOrSubstitutedAttributes => write!(f, "Ignored or substituted attributes"),
+ StatusCode::SuccessfulOkConflictingAttributes => write!(f, "Conflicting attributes"),
+ StatusCode::ClientErrorBadRequest => write!(f, "Bad request"),
+ StatusCode::ClientErrorForbidden => write!(f, "Forbidden"),
+ StatusCode::ClientErrorNotAuthenticated => write!(f, "Not authenticated"),
+ StatusCode::ClientErrorNotAuthorized => write!(f, "Not authorized"),
+ StatusCode::ClientErrorNotPossible => write!(f, "Not possible"),
+ StatusCode::ClientErrorTimeout => write!(f, "Timeout"),
+ StatusCode::ClientErrorNotFound => write!(f, "Not found"),
+ StatusCode::ClientErrorGone => write!(f, "Gone"),
+ StatusCode::ClientErrorRequestEntityTooLong => write!(f, "Entity too long"),
+ StatusCode::ClientErrorRequestValueTooLong => write!(f, "Request value too long"),
+ StatusCode::ClientErrorDocumentFormatNotSupported => write!(f, "Document format not supported"),
+ StatusCode::ClientErrorAttributesOrValuesNotSupported => write!(f, "Attributes or values not supported"),
+ StatusCode::ClientErrorUriSchemeNotSupported => write!(f, "Uri scheme not supported"),
+ StatusCode::ClientErrorCharsetNotSupported => write!(f, "Charset not supported"),
+ StatusCode::ClientErrorConflictingAttributes => write!(f, "Conflicting attributes"),
+ StatusCode::ClientErrorCompressionNotSupported => write!(f, "Compression not supported"),
+ StatusCode::ClientErrorCompressionError => write!(f, "Compression error"),
+ StatusCode::ClientErrorDocumentFormatError => write!(f, "Document format error"),
+ StatusCode::ClientErrorDocumentAccessError => write!(f, "Document access error"),
+ StatusCode::ServerErrorInternalError => write!(f, "Internal error"),
+ StatusCode::ServerErrorOperationNotSupported => write!(f, "Operation not supported"),
+ StatusCode::ServerErrorServiceUnavailable => write!(f, "Service unavailable"),
+ StatusCode::ServerErrorVersionNotSupported => write!(f, "Version not supported"),
+ StatusCode::ServerErrorDeviceError => write!(f, "Device error"),
+ StatusCode::ServerErrorTemporaryError => write!(f, "Temporary error"),
+ StatusCode::ServerErrorNotAcceptingJobs => write!(f, "Not accepting jobs"),
+ StatusCode::ServerErrorBusy => write!(f, "Busy"),
+ StatusCode::ServerErrorJobCanceled => write!(f, "Job canceled"),
+ StatusCode::ServerErrorMultipleDocumentJobsNotSupported => {
+ write!(f, "Multiple document jobs not supported")
+ }
+ StatusCode::UnknownStatusCode => write!(f, "Unknown status code"),
+ }
+ }
+}
diff --git a/crates/ipp/src/operation.rs b/crates/ipp/src/operation.rs
new file mode 100644
index 0000000..44a2f7e
--- /dev/null
+++ b/crates/ipp/src/operation.rs
@@ -0,0 +1,392 @@
+//!
+//! High-level IPP operation abstractions
+//!
+use http::Uri;
+
+use crate::{
+ attribute::IppAttribute,
+ model::{DelimiterTag, IppVersion, Operation},
+ payload::IppPayload,
+ request::IppRequestResponse,
+ value::IppValue,
+};
+
+pub mod builder;
+pub mod cups;
+
+fn with_user_name(user_name: Option<String>, req: &mut IppRequestResponse) {
+ if let Some(user_name) = user_name {
+ req.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(
+ IppAttribute::REQUESTING_USER_NAME,
+ IppValue::NameWithoutLanguage(user_name),
+ ),
+ );
+ }
+}
+
+/// Trait which represents a single IPP operation
+pub trait IppOperation {
+ /// Convert this operation to IPP request which is ready for sending
+ fn into_ipp_request(self) -> IppRequestResponse;
+
+ /// Return IPP version for this operation. Default is 1.1
+ fn version(&self) -> IppVersion {
+ IppVersion::v1_1()
+ }
+}
+
+impl<T: IppOperation> From<T> for IppRequestResponse {
+ fn from(op: T) -> Self {
+ op.into_ipp_request()
+ }
+}
+
+/// IPP operation Print-Job
+pub struct PrintJob {
+ printer_uri: Uri,
+ payload: IppPayload,
+ user_name: Option<String>,
+ job_name: Option<String>,
+ attributes: Vec<IppAttribute>,
+}
+
+impl PrintJob {
+ /// Create Print-Job operation
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `payload` - job payload<br/>
+ /// * `user_name` - name of the user (requesting-user-name)<br/>
+ /// * `job_name` - job name (job-name)<br/>
+ pub fn new<S, U, N>(printer_uri: Uri, payload: S, user_name: Option<U>, job_name: Option<N>) -> PrintJob
+ where
+ S: Into<IppPayload>,
+ U: AsRef<str>,
+ N: AsRef<str>,
+ {
+ PrintJob {
+ printer_uri,
+ payload: payload.into(),
+ user_name: user_name.map(|v| v.as_ref().to_string()),
+ job_name: job_name.map(|v| v.as_ref().to_string()),
+ attributes: Vec::new(),
+ }
+ }
+
+ /// Set extra job attribute for this operation, for example `colormodel=grayscale`
+ pub fn add_attribute(&mut self, attribute: IppAttribute) {
+ self.attributes.push(attribute);
+ }
+}
+
+impl IppOperation for PrintJob {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval = IppRequestResponse::new(self.version(), Operation::PrintJob, Some(self.printer_uri));
+
+ with_user_name(self.user_name, &mut retval);
+
+ if let Some(job_name) = self.job_name {
+ retval.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::JOB_NAME, IppValue::NameWithoutLanguage(job_name)),
+ )
+ }
+
+ for attr in self.attributes {
+ retval.attributes_mut().add(DelimiterTag::JobAttributes, attr);
+ }
+ *retval.payload_mut() = self.payload;
+
+ retval
+ }
+}
+
+/// IPP operation Get-Printer-Attributes
+pub struct GetPrinterAttributes {
+ printer_uri: Uri,
+ attributes: Vec<String>,
+}
+
+impl GetPrinterAttributes {
+ /// Create Get-Printer-Attributes operation to return all attributes
+ ///
+ /// * `printer_uri` - printer URI
+ pub fn new(printer_uri: Uri) -> GetPrinterAttributes {
+ GetPrinterAttributes {
+ printer_uri,
+ attributes: Vec::new(),
+ }
+ }
+
+ /// Create Get-Printer-Attributes operation to get a given list of attributes
+ ///
+ /// * `printer_uri` - printer URI
+ /// * `attributes` - list of attribute names to request from the printer
+ pub fn with_attributes<I, T>(printer_uri: Uri, attributes: I) -> GetPrinterAttributes
+ where
+ I: IntoIterator<Item = T>,
+ T: AsRef<str>,
+ {
+ GetPrinterAttributes {
+ printer_uri,
+ attributes: attributes.into_iter().map(|a| a.as_ref().to_string()).collect(),
+ }
+ }
+}
+
+impl IppOperation for GetPrinterAttributes {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval =
+ IppRequestResponse::new(self.version(), Operation::GetPrinterAttributes, Some(self.printer_uri));
+
+ if !self.attributes.is_empty() {
+ let vals: Vec<IppValue> = self.attributes.into_iter().map(IppValue::Keyword).collect();
+ retval.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::REQUESTED_ATTRIBUTES, IppValue::Array(vals)),
+ );
+ }
+
+ retval
+ }
+}
+
+/// IPP operation Create-Job
+pub struct CreateJob {
+ printer_uri: Uri,
+ job_name: Option<String>,
+ attributes: Vec<IppAttribute>,
+}
+
+impl CreateJob {
+ /// Create Create-Job operation
+ ///
+ /// * `printer_uri` - printer URI
+ /// * `job_name` - optional job name (job-name)<br/>
+ pub fn new<T>(printer_uri: Uri, job_name: Option<T>) -> CreateJob
+ where
+ T: AsRef<str>,
+ {
+ CreateJob {
+ printer_uri,
+ job_name: job_name.map(|v| v.as_ref().to_string()),
+ attributes: Vec::new(),
+ }
+ }
+
+ /// Set extra job attribute for this operation, for example `colormodel=grayscale`
+ pub fn add_attribute(&mut self, attribute: IppAttribute) {
+ self.attributes.push(attribute);
+ }
+}
+
+impl IppOperation for CreateJob {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval = IppRequestResponse::new(self.version(), Operation::CreateJob, Some(self.printer_uri));
+
+ if let Some(job_name) = self.job_name {
+ retval.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::JOB_NAME, IppValue::NameWithoutLanguage(job_name)),
+ )
+ }
+
+ for attr in self.attributes {
+ retval.attributes_mut().add(DelimiterTag::JobAttributes, attr);
+ }
+ retval
+ }
+}
+
+/// IPP operation Send-Document
+pub struct SendDocument {
+ printer_uri: Uri,
+ job_id: i32,
+ payload: IppPayload,
+ user_name: Option<String>,
+ last: bool,
+}
+
+impl SendDocument {
+ /// Create Send-Document operation
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `job_id` - job ID returned by Create-Job operation<br/>
+ /// * `payload` - `IppPayload`<br/>
+ /// * `user_name` - name of the user (requesting-user-name)<br/>
+ /// * `last` - whether this document is a last one<br/>
+ pub fn new<S, U>(printer_uri: Uri, job_id: i32, payload: S, user_name: Option<U>, last: bool) -> SendDocument
+ where
+ S: Into<IppPayload>,
+ U: AsRef<str>,
+ {
+ SendDocument {
+ printer_uri,
+ job_id,
+ payload: payload.into(),
+ user_name: user_name.map(|v| v.as_ref().to_string()),
+ last,
+ }
+ }
+}
+
+impl IppOperation for SendDocument {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval = IppRequestResponse::new(self.version(), Operation::SendDocument, Some(self.printer_uri));
+
+ retval.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::JOB_ID, IppValue::Integer(self.job_id)),
+ );
+
+ retval.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::LAST_DOCUMENT, IppValue::Boolean(self.last)),
+ );
+
+ with_user_name(self.user_name, &mut retval);
+
+ *retval.payload_mut() = self.payload;
+
+ retval
+ }
+}
+
+/// IPP operation Purge-Jobs
+pub struct PurgeJobs {
+ printer_uri: Uri,
+ user_name: Option<String>,
+}
+
+impl PurgeJobs {
+ /// Create Purge-Jobs operation
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `user_name` - name of the user (requesting-user-name)<br/>
+ pub fn new<U>(printer_uri: Uri, user_name: Option<U>) -> Self
+ where
+ U: AsRef<str>,
+ {
+ Self {
+ printer_uri,
+ user_name: user_name.map(|u| u.as_ref().to_owned()),
+ }
+ }
+}
+
+impl IppOperation for PurgeJobs {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval = IppRequestResponse::new(self.version(), Operation::PurgeJobs, Some(self.printer_uri));
+
+ with_user_name(self.user_name, &mut retval);
+
+ retval
+ }
+}
+
+/// IPP operation Cancel-Job
+pub struct CancelJob {
+ printer_uri: Uri,
+ job_id: i32,
+ user_name: Option<String>,
+}
+
+impl CancelJob {
+ /// Create Cancel-Job operation
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `job_id` - job ID<br/>
+ /// * `user_name` - name of the user (requesting-user-name)<br/>
+ pub fn new<U>(printer_uri: Uri, job_id: i32, user_name: Option<U>) -> Self
+ where
+ U: AsRef<str>,
+ {
+ Self {
+ printer_uri,
+ job_id,
+ user_name: user_name.map(|u| u.as_ref().to_owned()),
+ }
+ }
+}
+
+impl IppOperation for CancelJob {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval = IppRequestResponse::new(self.version(), Operation::CancelJob, Some(self.printer_uri));
+ retval.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::JOB_ID, IppValue::Integer(self.job_id)),
+ );
+ with_user_name(self.user_name, &mut retval);
+ retval
+ }
+}
+
+/// IPP operation Cancel-Job
+pub struct GetJobAttributes {
+ printer_uri: Uri,
+ job_id: i32,
+ user_name: Option<String>,
+}
+
+impl GetJobAttributes {
+ /// Create Get-Job-Attributes operation
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `job_id` - job ID<br/>
+ /// * `user_name` - name of the user (requesting-user-name)<br/>
+ pub fn new<U>(printer_uri: Uri, job_id: i32, user_name: Option<U>) -> Self
+ where
+ U: AsRef<str>,
+ {
+ Self {
+ printer_uri,
+ job_id,
+ user_name: user_name.map(|u| u.as_ref().to_owned()),
+ }
+ }
+}
+
+impl IppOperation for GetJobAttributes {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval = IppRequestResponse::new(self.version(), Operation::GetJobAttributes, Some(self.printer_uri));
+ retval.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::JOB_ID, IppValue::Integer(self.job_id)),
+ );
+ with_user_name(self.user_name, &mut retval);
+ retval
+ }
+}
+
+/// IPP operation Get-Jobs
+pub struct GetJobs {
+ printer_uri: Uri,
+ user_name: Option<String>,
+}
+
+impl GetJobs {
+ /// Create Get-Jobs operation
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `user_name` - name of the user (requesting-user-name)<br/>
+ pub fn new<U>(printer_uri: Uri, user_name: Option<U>) -> Self
+ where
+ U: AsRef<str>,
+ {
+ Self {
+ printer_uri,
+ user_name: user_name.map(|u| u.as_ref().to_owned()),
+ }
+ }
+}
+
+impl IppOperation for GetJobs {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ let mut retval = IppRequestResponse::new(self.version(), Operation::GetJobs, Some(self.printer_uri));
+
+ with_user_name(self.user_name, &mut retval);
+
+ retval
+ }
+}
diff --git a/crates/ipp/src/operation/builder.rs b/crates/ipp/src/operation/builder.rs
new file mode 100644
index 0000000..2bc2209
--- /dev/null
+++ b/crates/ipp/src/operation/builder.rs
@@ -0,0 +1,424 @@
+//!
+//! IPP operation builders
+//!
+use http::Uri;
+
+use crate::{
+ attribute::IppAttribute,
+ operation::{cups::*, *},
+ payload::IppPayload,
+};
+
+/// Builder to create IPP operations
+pub struct IppOperationBuilder;
+
+impl IppOperationBuilder {
+ /// Create Print-Job operation
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `payload` - `IppPayload`
+ pub fn print_job(printer_uri: Uri, payload: IppPayload) -> PrintJobBuilder {
+ PrintJobBuilder::new(printer_uri, payload)
+ }
+
+ /// Create Get-Printer-Attributes operation builder
+ ///
+ /// * `printer_uri` - printer URI
+ pub fn get_printer_attributes(printer_uri: Uri) -> GetPrinterAttributesBuilder {
+ GetPrinterAttributesBuilder::new(printer_uri)
+ }
+
+ /// Create Create-Job operation builder
+ ///
+ /// * `printer_uri` - printer URI
+ pub fn create_job(printer_uri: Uri) -> CreateJobBuilder {
+ CreateJobBuilder::new(printer_uri)
+ }
+
+ /// Create CUPS-specific operations
+ pub fn cups() -> CupsBuilder {
+ CupsBuilder::new()
+ }
+
+ /// Create Send-Document operation builder
+ ///
+ /// * `printer_uri` - printer URI<br/>
+ /// * `job_id` - job id returned by Create-Job operation <br/>
+ /// * `payload` - `IppPayload`
+ pub fn send_document(printer_uri: Uri, job_id: i32, payload: IppPayload) -> SendDocumentBuilder {
+ SendDocumentBuilder::new(printer_uri, job_id, payload)
+ }
+
+ /// Create Purge-Jobs operation builder
+ ///
+ /// * `printer_uri` - printer URI
+ pub fn purge_jobs(printer_uri: Uri) -> PurgeJobsBuilder {
+ PurgeJobsBuilder::new(printer_uri)
+ }
+
+ /// Create Cancel-Job operation builder
+ ///
+ /// * `printer_uri` - printer URI
+ /// * `job_id` - job id to cancel <br/>
+ pub fn cancel_job(printer_uri: Uri, job_id: i32) -> CancelJobBuilder {
+ CancelJobBuilder::new(printer_uri, job_id)
+ }
+
+ /// Create Get-Job-Attributes operation builder
+ ///
+ /// * `printer_uri` - printer URI
+ /// * `job_id` - job id to cancel <br/>
+ pub fn get_job_attributes(printer_uri: Uri, job_id: i32) -> GetJobAttributesBuilder {
+ GetJobAttributesBuilder::new(printer_uri, job_id)
+ }
+
+ /// Create Get-Jobs operation builder
+ ///
+ /// * `printer_uri` - printer URI
+ pub fn get_jobs(printer_uri: Uri) -> GetJobsBuilder {
+ GetJobsBuilder::new(printer_uri)
+ }
+}
+
+/// Builder to create Print-Job operation
+pub struct PrintJobBuilder {
+ printer_uri: Uri,
+ payload: IppPayload,
+ user_name: Option<String>,
+ job_title: Option<String>,
+ attributes: Vec<IppAttribute>,
+}
+
+impl PrintJobBuilder {
+ fn new(printer_uri: Uri, payload: IppPayload) -> PrintJobBuilder {
+ PrintJobBuilder {
+ printer_uri,
+ payload,
+ user_name: None,
+ job_title: None,
+ attributes: Vec::new(),
+ }
+ }
+ /// Specify requesting-user-name attribute
+ pub fn user_name<S>(mut self, user_name: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.user_name = Some(user_name.as_ref().to_owned());
+ self
+ }
+
+ /// Specify job-name attribute
+ pub fn job_title<S>(mut self, job_title: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.job_title = Some(job_title.as_ref().to_owned());
+ self
+ }
+
+ /// Specify custom job attribute
+ pub fn attribute(mut self, attribute: IppAttribute) -> Self {
+ self.attributes.push(attribute);
+ self
+ }
+
+ /// Specify custom job attributes
+ pub fn attributes<I>(mut self, attributes: I) -> Self
+ where
+ I: IntoIterator<Item = IppAttribute>,
+ {
+ self.attributes.extend(attributes);
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ let op = PrintJob::new(
+ self.printer_uri,
+ self.payload,
+ self.user_name.as_ref(),
+ self.job_title.as_ref(),
+ );
+ self.attributes.into_iter().fold(op, |mut op, attr| {
+ op.add_attribute(attr);
+ op
+ })
+ }
+}
+
+/// Builder to create Get-Printer-Attributes operation
+pub struct GetPrinterAttributesBuilder {
+ printer_uri: Uri,
+ attributes: Vec<String>,
+}
+
+impl GetPrinterAttributesBuilder {
+ fn new(printer_uri: Uri) -> GetPrinterAttributesBuilder {
+ GetPrinterAttributesBuilder {
+ printer_uri,
+ attributes: Vec::new(),
+ }
+ }
+
+ /// Specify which attribute to retrieve from the printer. Can be repeated.
+ pub fn attribute<S>(mut self, attribute: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.attributes.push(attribute.as_ref().to_owned());
+ self
+ }
+
+ /// Specify which attributes to retrieve from the printer
+ pub fn attributes<S, I>(mut self, attributes: I) -> Self
+ where
+ S: AsRef<str>,
+ I: IntoIterator<Item = S>,
+ {
+ self.attributes
+ .extend(attributes.into_iter().map(|s| s.as_ref().to_string()));
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ GetPrinterAttributes::with_attributes(self.printer_uri, &self.attributes)
+ }
+}
+
+/// Builder to create Create-Job operation
+pub struct CreateJobBuilder {
+ printer_uri: Uri,
+ job_name: Option<String>,
+ attributes: Vec<IppAttribute>,
+}
+
+impl CreateJobBuilder {
+ fn new(printer_uri: Uri) -> CreateJobBuilder {
+ CreateJobBuilder {
+ printer_uri,
+ job_name: None,
+ attributes: Vec::new(),
+ }
+ }
+
+ /// Specify job-name attribute
+ pub fn job_name<S>(mut self, job_name: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.job_name = Some(job_name.as_ref().to_owned());
+ self
+ }
+
+ /// Specify custom job attribute
+ pub fn attribute(mut self, attribute: IppAttribute) -> Self {
+ self.attributes.push(attribute);
+ self
+ }
+
+ /// Specify custom job attributes
+ pub fn attributes<I>(mut self, attributes: I) -> Self
+ where
+ I: IntoIterator<Item = IppAttribute>,
+ {
+ self.attributes.extend(attributes);
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ let op = CreateJob::new(self.printer_uri, self.job_name.as_ref());
+ self.attributes.into_iter().fold(op, |mut op, attr| {
+ op.add_attribute(attr);
+ op
+ })
+ }
+}
+
+/// Builder to create Send-Document operation
+pub struct SendDocumentBuilder {
+ printer_uri: Uri,
+ job_id: i32,
+ payload: IppPayload,
+ user_name: Option<String>,
+ is_last: bool,
+}
+
+impl SendDocumentBuilder {
+ fn new(printer_uri: Uri, job_id: i32, payload: IppPayload) -> SendDocumentBuilder {
+ SendDocumentBuilder {
+ printer_uri,
+ job_id,
+ payload,
+ user_name: None,
+ is_last: true,
+ }
+ }
+
+ /// Specify originating-user-name attribute
+ pub fn user_name<S>(mut self, user_name: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.user_name = Some(user_name.as_ref().to_owned());
+ self
+ }
+
+ /// Parameter which indicates whether this document is a last one
+ pub fn last(mut self, last: bool) -> Self {
+ self.is_last = last;
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ SendDocument::new(
+ self.printer_uri,
+ self.job_id,
+ self.payload,
+ self.user_name.as_ref(),
+ self.is_last,
+ )
+ }
+}
+
+/// Builder to create Purge-Jobs operation
+pub struct PurgeJobsBuilder {
+ printer_uri: Uri,
+ user_name: Option<String>,
+}
+
+impl PurgeJobsBuilder {
+ fn new(printer_uri: Uri) -> PurgeJobsBuilder {
+ PurgeJobsBuilder {
+ printer_uri,
+ user_name: None,
+ }
+ }
+
+ /// Specify originating-user-name attribute
+ pub fn user_name<S>(mut self, user_name: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.user_name = Some(user_name.as_ref().to_owned());
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ PurgeJobs::new(self.printer_uri, self.user_name)
+ }
+}
+
+/// Builder to create Cancel-Job operation
+pub struct CancelJobBuilder {
+ printer_uri: Uri,
+ job_id: i32,
+ user_name: Option<String>,
+}
+
+impl CancelJobBuilder {
+ fn new(printer_uri: Uri, job_id: i32) -> CancelJobBuilder {
+ CancelJobBuilder {
+ printer_uri,
+ job_id,
+ user_name: None,
+ }
+ }
+
+ /// Specify originating-user-name attribute
+ pub fn user_name<S>(mut self, user_name: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.user_name = Some(user_name.as_ref().to_owned());
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ CancelJob::new(self.printer_uri, self.job_id, self.user_name)
+ }
+}
+
+/// Builder to create Get-Job-Attributes operation
+pub struct GetJobAttributesBuilder {
+ printer_uri: Uri,
+ job_id: i32,
+ user_name: Option<String>,
+}
+
+impl GetJobAttributesBuilder {
+ fn new(printer_uri: Uri, job_id: i32) -> GetJobAttributesBuilder {
+ GetJobAttributesBuilder {
+ printer_uri,
+ job_id,
+ user_name: None,
+ }
+ }
+
+ /// Specify originating-user-name attribute
+ pub fn user_name<S>(mut self, user_name: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.user_name = Some(user_name.as_ref().to_owned());
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ GetJobAttributes::new(self.printer_uri, self.job_id, self.user_name)
+ }
+}
+
+/// Builder to create Get-Jobs operation
+pub struct GetJobsBuilder {
+ printer_uri: Uri,
+ user_name: Option<String>,
+}
+
+impl GetJobsBuilder {
+ fn new(printer_uri: Uri) -> GetJobsBuilder {
+ GetJobsBuilder {
+ printer_uri,
+ user_name: None,
+ }
+ }
+
+ /// Specify originating-user-name attribute
+ pub fn user_name<S>(mut self, user_name: S) -> Self
+ where
+ S: AsRef<str>,
+ {
+ self.user_name = Some(user_name.as_ref().to_owned());
+ self
+ }
+
+ /// Build operation
+ pub fn build(self) -> impl IppOperation {
+ GetJobs::new(self.printer_uri, self.user_name)
+ }
+}
+
+/// CUPS operations builder
+pub struct CupsBuilder;
+
+impl CupsBuilder {
+ fn new() -> CupsBuilder {
+ CupsBuilder
+ }
+
+ /// CUPS-Get-Printers operation
+ pub fn get_printers(&self) -> impl IppOperation {
+ CupsGetPrinters::new()
+ }
+
+ /// CUPS-Delete-Printer operation
+ pub fn delete_printer(&self, printer_uri: Uri) -> impl IppOperation {
+ CupsDeletePrinter::new(printer_uri)
+ }
+}
diff --git a/crates/ipp/src/operation/cups.rs b/crates/ipp/src/operation/cups.rs
new file mode 100644
index 0000000..c1b06d7
--- /dev/null
+++ b/crates/ipp/src/operation/cups.rs
@@ -0,0 +1,40 @@
+//!
+//! CUPS-specific IPP operations. For operations which require user authentication the URI may include authority part.
+//!
+
+use http::Uri;
+
+use crate::{model::Operation, operation::IppOperation, request::IppRequestResponse};
+
+/// IPP operation CUPS-Get-Printers
+#[derive(Default)]
+pub struct CupsGetPrinters;
+
+impl CupsGetPrinters {
+ /// Create CUPS-Get-Printers operation
+ pub fn new() -> CupsGetPrinters {
+ CupsGetPrinters
+ }
+}
+
+impl IppOperation for CupsGetPrinters {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ IppRequestResponse::new(self.version(), Operation::CupsGetPrinters, None)
+ }
+}
+
+/// IPP operation CUPS-Delete-Printer
+pub struct CupsDeletePrinter(Uri);
+
+impl CupsDeletePrinter {
+ /// Create CUPS-Get-Printers operation
+ pub fn new(printer_uri: Uri) -> CupsDeletePrinter {
+ CupsDeletePrinter(printer_uri)
+ }
+}
+
+impl IppOperation for CupsDeletePrinter {
+ fn into_ipp_request(self) -> IppRequestResponse {
+ IppRequestResponse::new(self.version(), Operation::CupsDeletePrinter, Some(self.0))
+ }
+}
diff --git a/crates/ipp/src/parser.rs b/crates/ipp/src/parser.rs
new file mode 100644
index 0000000..efdbb5a
--- /dev/null
+++ b/crates/ipp/src/parser.rs
@@ -0,0 +1,583 @@
+//!
+//! IPP stream parser
+//!
+use std::{
+ collections::BTreeMap,
+ io::{self, Read},
+};
+
+use bytes::Bytes;
+use log::{error, trace};
+
+#[cfg(feature = "async")]
+use {crate::reader::AsyncIppReader, futures_util::io::AsyncRead};
+
+use crate::{
+ attribute::{IppAttribute, IppAttributeGroup, IppAttributes},
+ model::{DelimiterTag, ValueTag},
+ reader::IppReader,
+ request::IppRequestResponse,
+ value::IppValue,
+ FromPrimitive as _, IppHeader,
+};
+
+/// Parse error enum
+#[derive(Debug, thiserror::Error)]
+pub enum IppParseError {
+ #[error("Invalid tag: {0}")]
+ InvalidTag(u8),
+
+ #[error("Invalid IPP collection")]
+ InvalidCollection,
+
+ #[error(transparent)]
+ IoError(#[from] io::Error),
+}
+
+// create a single value from one-element list, list otherwise
+fn list_or_value(mut list: Vec<IppValue>) -> IppValue {
+ if list.len() == 1 {
+ list.remove(0)
+ } else {
+ IppValue::Array(list)
+ }
+}
+
+struct ParserState {
+ current_group: Option<IppAttributeGroup>,
+ last_name: Option<String>,
+ context: Vec<Vec<IppValue>>,
+ attributes: IppAttributes,
+}
+
+impl ParserState {
+ fn new() -> Self {
+ ParserState {
+ current_group: None,
+ last_name: None,
+ context: vec![vec![]],
+ attributes: IppAttributes::new(),
+ }
+ }
+
+ fn add_last_attribute(&mut self) {
+ if let Some(last_name) = self.last_name.take() {
+ if let Some(val_list) = self.context.pop() {
+ if let Some(ref mut group) = self.current_group {
+ let attr = IppAttribute::new(&last_name, list_or_value(val_list));
+ group.attributes_mut().insert(last_name, attr);
+ }
+ }
+ self.context.push(vec![]);
+ }
+ }
+
+ fn parse_delimiter(&mut self, tag: u8) -> Result<DelimiterTag, IppParseError> {
+ trace!("Delimiter tag: {:0x}", tag);
+
+ let tag = DelimiterTag::from_u8(tag).ok_or(IppParseError::InvalidTag(tag))?;
+
+ self.add_last_attribute();
+
+ if let Some(group) = self.current_group.take() {
+ self.attributes.groups_mut().push(group);
+ }
+
+ self.current_group = Some(IppAttributeGroup::new(tag));
+
+ Ok(tag)
+ }
+
+ fn parse_value(&mut self, tag: u8, name: String, value: Bytes) -> Result<(), IppParseError> {
+ let ipp_value = IppValue::parse(tag, value)?;
+
+ trace!("Value tag: {:0x}: {}: {}", tag, name, ipp_value);
+
+ if !name.is_empty() {
+ // single attribute or begin of array
+ self.add_last_attribute();
+ // store it as a previous attribute
+ self.last_name = Some(name);
+ }
+ if tag == ValueTag::BegCollection as u8 {
+ // start new collection in the stack
+ trace!("Begin collection");
+ match ipp_value {
+ IppValue::Other { ref data, .. } if data.is_empty() => {}
+ _ => {
+ error!("Invalid begin collection attribute");
+ return Err(IppParseError::InvalidCollection);
+ }
+ }
+ self.context.push(vec![]);
+ } else if tag == ValueTag::EndCollection as u8 {
+ // get collection from the stack and add it to the previous element
+ trace!("End collection");
+ match ipp_value {
+ IppValue::Other { ref data, .. } if data.is_empty() => {}
+ _ => {
+ error!("Invalid end collection attribute");
+ return Err(IppParseError::InvalidCollection);
+ }
+ }
+ if let Some(arr) = self.context.pop() {
+ if let Some(val_list) = self.context.last_mut() {
+ let mut map: BTreeMap<String, IppValue> = BTreeMap::new();
+ for idx in (0..arr.len()).step_by(2) {
+ if let (Some(IppValue::MemberAttrName(k)), Some(v)) = (arr.get(idx), arr.get(idx + 1)) {
+ map.insert(k.to_string(), v.clone());
+ }
+ }
+ val_list.push(IppValue::Collection(map));
+ }
+ }
+ } else if let Some(val_list) = self.context.last_mut() {
+ // add attribute to the current collection
+ val_list.push(ipp_value);
+ }
+ Ok(())
+ }
+}
+
+#[cfg(feature = "async")]
+/// Asynchronous IPP parser
+pub struct AsyncIppParser<R> {
+ reader: AsyncIppReader<R>,
+ state: ParserState,
+}
+
+#[cfg(feature = "async")]
+impl<R> AsyncIppParser<R>
+where
+ R: AsyncRead + Send + Sync + Unpin,
+{
+ /// Create IPP parser from AsyncIppReader
+ pub fn new<T>(reader: T) -> AsyncIppParser<R>
+ where
+ T: Into<AsyncIppReader<R>>,
+ {
+ AsyncIppParser {
+ reader: reader.into(),
+ state: ParserState::new(),
+ }
+ }
+
+ async fn parse_value(&mut self, tag: u8) -> Result<(), IppParseError> {
+ // value tag
+ let name = self.reader.read_name().await?;
+ let value = self.reader.read_value().await?;
+
+ self.state.parse_value(tag, name, value)
+ }
+
+ async fn parse_header_attributes(&mut self) -> Result<IppHeader, IppParseError> {
+ let header = self.reader.read_header().await?;
+ trace!("IPP header: {:?}", header);
+
+ loop {
+ match self.reader.read_tag().await? {
+ tag @ 0x01..=0x05 => {
+ if self.state.parse_delimiter(tag)? == DelimiterTag::EndOfAttributes {
+ break;
+ }
+ }
+ tag @ 0x10..=0x4a => self.parse_value(tag).await?,
+ tag => {
+ return Err(IppParseError::InvalidTag(tag));
+ }
+ }
+ }
+
+ Ok(header)
+ }
+
+ /// Parse IPP stream without reading beyond the end of the attributes. The payload stays untouched.
+ pub async fn parse_parts(mut self) -> Result<(IppHeader, IppAttributes, AsyncIppReader<R>), IppParseError> {
+ let header = self.parse_header_attributes().await?;
+ Ok((header, self.state.attributes, self.reader))
+ }
+
+ /// Parse IPP stream
+ pub async fn parse(mut self) -> Result<IppRequestResponse, IppParseError>
+ where
+ R: 'static,
+ {
+ let header = self.parse_header_attributes().await?;
+
+ Ok(IppRequestResponse {
+ header,
+ attributes: self.state.attributes,
+ payload: self.reader.into_payload(),
+ })
+ }
+}
+
+/// Synchronous IPP parser
+pub struct IppParser<R> {
+ reader: IppReader<R>,
+ state: ParserState,
+}
+
+impl<R> IppParser<R>
+where
+ R: 'static + Read + Send + Sync,
+{
+ /// Create IPP parser from IppReader
+ pub fn new<T>(reader: T) -> IppParser<R>
+ where
+ T: Into<IppReader<R>>,
+ {
+ IppParser {
+ reader: reader.into(),
+ state: ParserState::new(),
+ }
+ }
+
+ fn parse_value(&mut self, tag: u8) -> Result<(), IppParseError> {
+ // value tag
+ let name = self.reader.read_name()?;
+ let value = self.reader.read_value()?;
+
+ self.state.parse_value(tag, name, value)
+ }
+
+ fn parse_header_attributes(&mut self) -> Result<IppHeader, IppParseError> {
+ let header = self.reader.read_header()?;
+ trace!("IPP header: {:?}", header);
+
+ loop {
+ match self.reader.read_tag()? {
+ tag @ 0x01..=0x05 => {
+ if self.state.parse_delimiter(tag)? == DelimiterTag::EndOfAttributes {
+ break;
+ }
+ }
+ tag @ 0x10..=0x4a => self.parse_value(tag)?,
+ tag => {
+ return Err(IppParseError::InvalidTag(tag));
+ }
+ }
+ }
+
+ Ok(header)
+ }
+
+ /// Parse IPP stream without reading beyond the end of the attributes. The payload stays untouched.
+ pub fn parse_parts(mut self) -> Result<(IppHeader, IppAttributes, IppReader<R>), IppParseError> {
+ let header = self.parse_header_attributes()?;
+ Ok((header, self.state.attributes, self.reader))
+ }
+
+ /// Parse IPP stream
+ pub fn parse(mut self) -> Result<IppRequestResponse, IppParseError>
+ where
+ R: 'static,
+ {
+ let header = self.parse_header_attributes()?;
+
+ Ok(IppRequestResponse {
+ header,
+ attributes: self.state.attributes,
+ payload: self.reader.into_payload(),
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::prelude::IppVersion;
+
+ use super::*;
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_parse_no_attributes() {
+ let data = &[1, 1, 0, 0, 0, 0, 0, 0, 3];
+ let result = AsyncIppParser::new(AsyncIppReader::new(futures_util::io::Cursor::new(data)))
+ .parse()
+ .await;
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ assert!(res.attributes.groups().is_empty());
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_parse_single_value() {
+ let data = &[
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78, 3,
+ ];
+ let result = AsyncIppParser::new(AsyncIppReader::new(futures_util::io::Cursor::new(data)))
+ .parse()
+ .await;
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(attr.value().as_integer(), Some(&0x1234_5678));
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_parse_array() {
+ let data = &[
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78,
+ 0x21, 0x00, 0x00, 0x00, 0x04, 0x77, 0x65, 0x43, 0x21, 3,
+ ];
+ let result = AsyncIppParser::new(AsyncIppReader::new(futures_util::io::Cursor::new(data)))
+ .parse()
+ .await;
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(
+ attr.value().as_array(),
+ Some(&vec![IppValue::Integer(0x1234_5678), IppValue::Integer(0x7765_4321)])
+ );
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_parse_collection() {
+ let data = vec![
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x34, 0, 4, b'c', b'o', b'l', b'l', 0, 0, 0x4a, 0, 0, 0, 4, b'a', b'b', b'c',
+ b'd', 0x44, 0, 0, 0, 3, b'k', b'e', b'y', 0x37, 0, 0, 0, 0, 3,
+ ];
+ let result = AsyncIppParser::new(AsyncIppReader::new(futures_util::io::Cursor::new(data)))
+ .parse()
+ .await;
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("coll").unwrap();
+ assert_eq!(
+ attr.value(),
+ &IppValue::Collection(BTreeMap::from([(
+ "abcd".to_string(),
+ IppValue::Keyword("key".to_owned())
+ )]))
+ );
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_parse_with_payload() {
+ let data = vec![
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78, 3,
+ b'f', b'o', b'o',
+ ];
+
+ let result = AsyncIppParser::new(AsyncIppReader::new(futures_util::io::Cursor::new(data)))
+ .parse()
+ .await;
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ assert_eq!(res.header.version, IppVersion::v1_1());
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(attr.value().as_integer(), Some(&0x1234_5678));
+
+ let mut cursor = futures_util::io::Cursor::new(Vec::new());
+ futures_executor::block_on(futures_util::io::copy(res.payload, &mut cursor)).unwrap();
+ assert_eq!(cursor.into_inner(), b"foo");
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_parse_parts() {
+ let data = vec![
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78, 3,
+ b'f', b'o', b'o',
+ ];
+
+ let result = AsyncIppParser::new(AsyncIppReader::new(futures_util::io::Cursor::new(data)))
+ .parse_parts()
+ .await;
+ assert!(result.is_ok());
+
+ let (header, attributes, reader) = result.ok().unwrap();
+ assert_eq!(header.version, IppVersion::v1_1());
+ let attrs = attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(attr.value().as_integer(), Some(&0x1234_5678));
+
+ let mut payload = reader.into_payload();
+ let mut cursor = io::Cursor::new(Vec::new());
+ io::copy(&mut payload, &mut cursor).unwrap();
+ assert_eq!(cursor.into_inner(), b"foo");
+ }
+
+ #[test]
+ fn test_parse_no_attributes() {
+ let data = &[1, 1, 0, 0, 0, 0, 0, 0, 3];
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse();
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ assert!(res.attributes.groups().is_empty());
+ }
+
+ #[test]
+ fn test_parse_single_value() {
+ let data = &[
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78, 3,
+ ];
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse();
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(attr.value().as_integer(), Some(&0x1234_5678));
+ }
+
+ #[test]
+ fn test_parse_array() {
+ let data = &[
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78,
+ 0x21, 0x00, 0x00, 0x00, 0x04, 0x77, 0x65, 0x43, 0x21, 3,
+ ];
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse();
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(
+ attr.value().as_array(),
+ Some(&vec![IppValue::Integer(0x1234_5678), IppValue::Integer(0x7765_4321)])
+ );
+ }
+
+ #[test]
+ fn test_parse_collection() {
+ let data = vec![
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x34, 0, 4, b'c', b'o', b'l', b'l', 0, 0, 0x4a, 0, 0, 0, 4, b'a', b'b', b'c',
+ b'd', 0x44, 0, 0, 0, 3, b'k', b'e', b'y', 0x37, 0, 0, 0, 0, 3,
+ ];
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse();
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("coll").unwrap();
+ assert_eq!(
+ attr.value(),
+ &IppValue::Collection(BTreeMap::from([(
+ "abcd".to_string(),
+ IppValue::Keyword("key".to_owned())
+ )]))
+ );
+ }
+
+ #[test]
+ fn test_parser_with_payload() {
+ let data = vec![
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78, 3,
+ b'f', b'o', b'o',
+ ];
+
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse();
+ assert!(result.is_ok());
+
+ let mut res = result.ok().unwrap();
+ assert_eq!(res.header.version, IppVersion::v1_1());
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(attr.value().as_integer(), Some(&0x1234_5678));
+
+ let mut cursor = io::Cursor::new(Vec::new());
+ io::copy(&mut res.payload, &mut cursor).unwrap();
+ assert_eq!(cursor.into_inner(), b"foo");
+ }
+
+ #[test]
+ fn test_parse_parts() {
+ let data = vec![
+ 1, 1, 0, 0, 0, 0, 0, 0, 4, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x12, 0x34, 0x56, 0x78, 3,
+ b'f', b'o', b'o',
+ ];
+
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse_parts();
+ assert!(result.is_ok());
+
+ let (header, attributes, reader) = result.ok().unwrap();
+ assert_eq!(header.version, IppVersion::v1_1());
+ let attrs = attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("test").unwrap();
+ assert_eq!(attr.value().as_integer(), Some(&0x1234_5678));
+
+ let mut payload = reader.into_payload();
+ let mut cursor = io::Cursor::new(Vec::new());
+ io::copy(&mut payload, &mut cursor).unwrap();
+ assert_eq!(cursor.into_inner(), b"foo");
+ }
+
+ #[test]
+ fn test_parse_groups() {
+ let data = vec![
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04,
+ 0x12, 0x34, 0x56, 0x78, 0x21, 0x00, 0x05, b't', b'e', b's', b't', b'2', 0x00, 0x04, 0x12, 0x34, 0x56, 0xFF,
+ 0x04, 0x21, 0x00, 0x04, b't', b'e', b's', b't', 0x00, 0x04, 0x87, 0x65, 0x43, 0x21, 0x03,
+ ];
+
+ let res = IppParser::new(IppReader::new(io::Cursor::new(data))).parse().unwrap();
+
+ assert_eq!(2, res.attributes().groups()[0].attributes().len());
+ assert_eq!(1, res.attributes().groups()[1].attributes().len());
+ }
+}
diff --git a/crates/ipp/src/payload.rs b/crates/ipp/src/payload.rs
new file mode 100644
index 0000000..af68cf6
--- /dev/null
+++ b/crates/ipp/src/payload.rs
@@ -0,0 +1,85 @@
+//!
+//! IPP payload
+//!
+use std::io::{self, Read};
+
+#[cfg(feature = "async")]
+use {
+ futures_util::io::{AllowStdIo, AsyncRead, AsyncReadExt},
+ std::{
+ pin::Pin,
+ task::{Context, Poll},
+ },
+};
+
+enum PayloadKind {
+ #[cfg(feature = "async")]
+ Async(Box<dyn AsyncRead + Send + Sync + Unpin>),
+ Sync(Box<dyn Read + Send + Sync>),
+ Empty,
+}
+
+/// IPP payload
+pub struct IppPayload {
+ inner: PayloadKind,
+}
+
+impl IppPayload {
+ /// Create empty payload
+ pub fn empty() -> Self {
+ IppPayload {
+ inner: PayloadKind::Empty,
+ }
+ }
+
+ #[cfg(feature = "async")]
+ /// Create an async payload from the AsyncRead object
+ pub fn new_async<R>(r: R) -> Self
+ where
+ R: 'static + AsyncRead + Send + Sync + Unpin,
+ {
+ IppPayload {
+ inner: PayloadKind::Async(Box::new(r)),
+ }
+ }
+
+ /// Create a sync payload from the Read object
+ pub fn new<R>(r: R) -> Self
+ where
+ R: 'static + Read + Send + Sync,
+ {
+ IppPayload {
+ inner: PayloadKind::Sync(Box::new(r)),
+ }
+ }
+}
+
+impl Default for IppPayload {
+ fn default() -> Self {
+ Self {
+ inner: PayloadKind::Empty,
+ }
+ }
+}
+
+#[cfg(feature = "async")]
+impl AsyncRead for IppPayload {
+ fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context, buf: &mut [u8]) -> Poll<io::Result<usize>> {
+ match self.inner {
+ PayloadKind::Async(ref mut inner) => Pin::new(&mut *inner).poll_read(cx, buf),
+ PayloadKind::Sync(ref mut inner) => Pin::new(&mut AllowStdIo::new(inner)).poll_read(cx, buf),
+ PayloadKind::Empty => Poll::Ready(Ok(0)),
+ }
+ }
+}
+
+impl Read for IppPayload {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ match self.inner {
+ #[cfg(feature = "async")]
+ PayloadKind::Async(ref mut inner) => futures_executor::block_on(inner.read(buf)),
+ PayloadKind::Sync(ref mut inner) => inner.read(buf),
+ PayloadKind::Empty => Ok(0),
+ }
+ }
+}
diff --git a/crates/ipp/src/reader.rs b/crates/ipp/src/reader.rs
new file mode 100644
index 0000000..d92d5a1
--- /dev/null
+++ b/crates/ipp/src/reader.rs
@@ -0,0 +1,270 @@
+//!
+//! IPP reader
+//!
+use std::io::{self, Read};
+
+use bytes::Bytes;
+
+#[cfg(feature = "async")]
+use futures_util::io::{AsyncRead, AsyncReadExt};
+
+use crate::{model::IppVersion, payload::IppPayload, IppHeader};
+
+#[cfg(feature = "async")]
+/// Asynchronous IPP reader contains a set of methods to read from IPP data stream
+pub struct AsyncIppReader<R> {
+ inner: R,
+}
+
+#[cfg(feature = "async")]
+impl<R> AsyncIppReader<R>
+where
+ R: AsyncRead + Send + Sync + Unpin,
+{
+ /// Create IppReader from AsyncRead instance
+ pub fn new(inner: R) -> Self {
+ AsyncIppReader { inner }
+ }
+
+ async fn read_bytes(&mut self, len: usize) -> io::Result<Bytes> {
+ let mut buf = vec![0; len];
+ self.inner.read_exact(&mut buf).await?;
+ Ok(buf.into())
+ }
+
+ async fn read_string(&mut self, len: usize) -> io::Result<String> {
+ self.read_bytes(len)
+ .await
+ .map(|b| String::from_utf8_lossy(&b).into_owned())
+ }
+
+ async fn read_u16(&mut self) -> io::Result<u16> {
+ let mut buf = [0u8; 2];
+ self.inner.read_exact(&mut buf).await?;
+ Ok(u16::from_be_bytes(buf))
+ }
+
+ async fn read_u8(&mut self) -> io::Result<u8> {
+ let mut buf = [0u8; 1];
+ self.inner.read_exact(&mut buf).await?;
+ Ok(buf[0])
+ }
+
+ async fn read_u32(&mut self) -> io::Result<u32> {
+ let mut buf = [0u8; 4];
+ self.inner.read_exact(&mut buf).await?;
+ Ok(u32::from_be_bytes(buf))
+ }
+
+ /// Read tag
+ pub async fn read_tag(&mut self) -> io::Result<u8> {
+ self.read_u8().await
+ }
+
+ /// Read IPP name from [len; name] element
+ pub async fn read_name(&mut self) -> io::Result<String> {
+ let name_len = self.read_u16().await?;
+ self.read_string(name_len as usize).await
+ }
+
+ /// Read IPP value from [len; value] element
+ pub async fn read_value(&mut self) -> io::Result<Bytes> {
+ let value_len = self.read_u16().await?;
+ self.read_bytes(value_len as usize).await
+ }
+
+ /// Read IPP header
+ pub async fn read_header(&mut self) -> io::Result<IppHeader> {
+ let version = IppVersion(self.read_u16().await?);
+ let operation_status = self.read_u16().await?;
+ let request_id = self.read_u32().await?;
+
+ Ok(IppHeader::new(version, operation_status, request_id))
+ }
+
+ /// Release the underlying reader
+ pub fn into_inner(self) -> R {
+ self.inner
+ }
+
+ /// Convert the remaining inner stream into IppPayload
+ pub fn into_payload(self) -> IppPayload
+ where
+ R: 'static,
+ {
+ IppPayload::new_async(self.inner)
+ }
+}
+
+#[cfg(feature = "async")]
+impl<R> From<R> for AsyncIppReader<R>
+where
+ R: AsyncRead + Send + Sync + Unpin,
+{
+ fn from(r: R) -> Self {
+ AsyncIppReader::new(r)
+ }
+}
+
+/// Synchronous IPP reader contains a set of methods to read from IPP data stream
+pub struct IppReader<R> {
+ inner: R,
+}
+
+impl<R> IppReader<R>
+where
+ R: Read + Send + Sync,
+{
+ /// Create IppReader from Read instance
+ pub fn new(inner: R) -> Self {
+ IppReader { inner }
+ }
+
+ fn read_bytes(&mut self, len: usize) -> io::Result<Bytes> {
+ let mut buf = vec![0; len];
+ self.inner.read_exact(&mut buf)?;
+ Ok(buf.into())
+ }
+
+ fn read_string(&mut self, len: usize) -> io::Result<String> {
+ self.read_bytes(len).map(|b| String::from_utf8_lossy(&b).into_owned())
+ }
+
+ fn read_u16(&mut self) -> io::Result<u16> {
+ let mut buf = [0u8; 2];
+ self.inner.read_exact(&mut buf)?;
+ Ok(u16::from_be_bytes(buf))
+ }
+
+ fn read_u8(&mut self) -> io::Result<u8> {
+ let mut buf = [0u8; 1];
+ self.inner.read_exact(&mut buf)?;
+ Ok(buf[0])
+ }
+
+ fn read_u32(&mut self) -> io::Result<u32> {
+ let mut buf = [0u8; 4];
+ self.inner.read_exact(&mut buf)?;
+ Ok(u32::from_be_bytes(buf))
+ }
+
+ /// Read tag
+ pub fn read_tag(&mut self) -> io::Result<u8> {
+ self.read_u8()
+ }
+
+ /// Read IPP name from [len; name] element
+ pub fn read_name(&mut self) -> io::Result<String> {
+ let name_len = self.read_u16()?;
+ self.read_string(name_len as usize)
+ }
+
+ /// Read IPP value from [len; value] element
+ pub fn read_value(&mut self) -> io::Result<Bytes> {
+ let value_len = self.read_u16()?;
+ self.read_bytes(value_len as usize)
+ }
+
+ /// Read IPP header
+ pub fn read_header(&mut self) -> io::Result<IppHeader> {
+ let version = IppVersion(self.read_u16()?);
+ let operation_status = self.read_u16()?;
+ let request_id = self.read_u32()?;
+
+ Ok(IppHeader::new(version, operation_status, request_id))
+ }
+
+ /// Release the underlying reader
+ pub fn into_inner(self) -> R {
+ self.inner
+ }
+
+ /// Convert the remaining inner stream into IppPayload
+ pub fn into_payload(self) -> IppPayload
+ where
+ R: 'static,
+ {
+ IppPayload::new(self.inner)
+ }
+}
+
+impl<R> From<R> for IppReader<R>
+where
+ R: Read + Send + Sync,
+{
+ fn from(r: R) -> Self {
+ IppReader::new(r)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::model::StatusCode;
+
+ #[test]
+ fn test_read_name() {
+ let data = io::Cursor::new(vec![0x00, 0x04, b't', b'e', b's', b't']);
+ let mut reader = IppReader::new(data);
+ let name = reader.read_name().unwrap();
+ assert_eq!(name, "test");
+ }
+
+ #[test]
+ fn test_read_value() {
+ let data = io::Cursor::new(vec![0x00, 0x04, b't', b'e', b's', b't']);
+ let mut reader = IppReader::new(data);
+ let value = reader.read_value().unwrap();
+ assert_eq!(value.as_ref(), b"test");
+ }
+
+ #[test]
+ fn test_read_borrowed_value() {
+ let data = vec![0x00, 0x04, b't', b'e', b's', b't'];
+ let data = io::Cursor::new(&data);
+ let mut reader = IppReader::new(data);
+ let value = reader.read_value().unwrap();
+ assert_eq!(value.as_ref(), b"test");
+ }
+
+ #[test]
+ fn test_read_header() {
+ let data = io::Cursor::new(vec![0x01, 0x01, 0x04, 0x01, 0x11, 0x22, 0x33, 0x44]);
+ let mut reader = IppReader::new(data);
+ let header = reader.read_header().unwrap();
+ assert_eq!(header.version, IppVersion::v1_1());
+ assert_eq!(header.operation_or_status, 0x401);
+ assert_eq!(header.request_id, 0x11223344);
+ assert_eq!(header.status_code(), StatusCode::ClientErrorForbidden);
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_read_name() {
+ let data = futures_util::io::Cursor::new(vec![0x00, 0x04, b't', b'e', b's', b't']);
+ let mut reader = AsyncIppReader::new(data);
+ let name = reader.read_name().await.unwrap();
+ assert_eq!(name, "test");
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_read_value() {
+ let data = futures_util::io::Cursor::new(vec![0x00, 0x04, b't', b'e', b's', b't']);
+ let mut reader = AsyncIppReader::new(data);
+ let value = reader.read_value().await.unwrap();
+ assert_eq!(value.as_ref(), b"test");
+ }
+
+ #[cfg(feature = "async")]
+ #[tokio::test]
+ async fn test_async_read_header() {
+ let data = futures_util::io::Cursor::new(vec![0x01, 0x01, 0x04, 0x01, 0x11, 0x22, 0x33, 0x44]);
+ let mut reader = AsyncIppReader::new(data);
+ let header = reader.read_header().await.unwrap();
+ assert_eq!(header.version, IppVersion::v1_1());
+ assert_eq!(header.operation_or_status, 0x401);
+ assert_eq!(header.request_id, 0x11223344);
+ assert_eq!(header.status_code(), StatusCode::ClientErrorForbidden);
+ }
+}
diff --git a/crates/ipp/src/request.rs b/crates/ipp/src/request.rs
new file mode 100644
index 0000000..0ac280c
--- /dev/null
+++ b/crates/ipp/src/request.rs
@@ -0,0 +1,150 @@
+//!
+//! IPP request
+//!
+use std::io::{self, Read};
+
+use bytes::{BufMut, Bytes, BytesMut};
+#[cfg(feature = "async")]
+use futures_util::io::{AsyncRead, AsyncReadExt};
+use http::Uri;
+use log::trace;
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ attribute::{IppAttribute, IppAttributes},
+ model::{DelimiterTag, IppVersion, Operation, StatusCode},
+ payload::IppPayload,
+ value::*,
+ IppHeader,
+};
+
+/// IPP request/response struct
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct IppRequestResponse {
+ pub(crate) header: IppHeader,
+ pub(crate) attributes: IppAttributes,
+ #[cfg_attr(feature = "serde", serde(skip))]
+ pub(crate) payload: IppPayload,
+}
+
+impl IppRequestResponse {
+ /// Create new IPP request for the operation and uri
+ pub fn new(version: IppVersion, operation: Operation, uri: Option<Uri>) -> IppRequestResponse {
+ let header = IppHeader::new(version, operation as u16, 1);
+ let mut attributes = IppAttributes::new();
+
+ attributes.add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::ATTRIBUTES_CHARSET, IppValue::Charset("utf-8".to_string())),
+ );
+
+ attributes.add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(
+ IppAttribute::ATTRIBUTES_NATURAL_LANGUAGE,
+ IppValue::NaturalLanguage("en".to_string()),
+ ),
+ );
+
+ if let Some(uri) = uri {
+ attributes.add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(
+ IppAttribute::PRINTER_URI,
+ IppValue::Uri(crate::util::canonicalize_uri(&uri).to_string()),
+ ),
+ );
+ }
+
+ IppRequestResponse {
+ header,
+ attributes,
+ payload: IppPayload::empty(),
+ }
+ }
+
+ /// Create response from status and id
+ pub fn new_response(version: IppVersion, status: StatusCode, id: u32) -> IppRequestResponse {
+ let header = IppHeader::new(version, status as u16, id);
+ let mut response = IppRequestResponse {
+ header,
+ attributes: IppAttributes::new(),
+ payload: IppPayload::empty(),
+ };
+
+ response.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(IppAttribute::ATTRIBUTES_CHARSET, IppValue::Charset("utf-8".to_string())),
+ );
+ response.attributes_mut().add(
+ DelimiterTag::OperationAttributes,
+ IppAttribute::new(
+ IppAttribute::ATTRIBUTES_NATURAL_LANGUAGE,
+ IppValue::NaturalLanguage("en".to_string()),
+ ),
+ );
+
+ response
+ }
+
+ /// Get IPP header
+ pub fn header(&self) -> &IppHeader {
+ &self.header
+ }
+
+ /// Get mutable IPP header
+ pub fn header_mut(&mut self) -> &mut IppHeader {
+ &mut self.header
+ }
+
+ /// Get attributes
+ pub fn attributes(&self) -> &IppAttributes {
+ &self.attributes
+ }
+
+ /// Get attributes
+ pub fn attributes_mut(&mut self) -> &mut IppAttributes {
+ &mut self.attributes
+ }
+
+ /// Get payload
+ pub fn payload(&self) -> &IppPayload {
+ &self.payload
+ }
+
+ /// Get mutable payload
+ pub fn payload_mut(&mut self) -> &mut IppPayload {
+ &mut self.payload
+ }
+
+ /// Write request to byte array not including payload
+ pub fn to_bytes(&self) -> Bytes {
+ let mut buffer = BytesMut::new();
+ buffer.put(self.header.to_bytes());
+ buffer.put(self.attributes.to_bytes());
+ buffer.freeze()
+ }
+
+ #[cfg(feature = "async")]
+ /// Convert request/response into AsyncRead including payload
+ pub fn into_async_read(self) -> impl AsyncRead + Send + Sync + 'static {
+ let header = self.to_bytes();
+ trace!("IPP header size: {}", header.len(),);
+
+ futures_util::io::Cursor::new(header).chain(self.payload)
+ }
+
+ /// Convert request/response into Read including payload
+ pub fn into_read(self) -> impl Read + Send + Sync + 'static {
+ let header = self.to_bytes();
+ trace!("IPP header size: {}", header.len(),);
+
+ io::Cursor::new(header).chain(self.payload)
+ }
+
+ /// Consume request/response and return a payload
+ pub fn into_payload(self) -> IppPayload {
+ self.payload
+ }
+}
diff --git a/crates/ipp/src/util.rs b/crates/ipp/src/util.rs
new file mode 100644
index 0000000..ee389c0
--- /dev/null
+++ b/crates/ipp/src/util.rs
@@ -0,0 +1,96 @@
+//!
+//! IPP helper functions
+//!
+use http::Uri;
+use num_traits::FromPrimitive;
+
+use crate::{
+ attribute::IppAttribute,
+ error::IppError,
+ model::{DelimiterTag, PrinterState},
+ prelude::IppRequestResponse,
+};
+
+/// convert `http://username:pwd@host:port/path?query` into `ipp://host:port/path`
+pub fn canonicalize_uri(uri: &Uri) -> Uri {
+ let mut builder = Uri::builder().scheme("ipp").path_and_query(uri.path());
+ if let Some(authority) = uri.authority() {
+ if let Some(port) = authority.port_u16() {
+ builder = builder.authority(format!("{}:{}", authority.host(), port).as_str());
+ } else {
+ builder = builder.authority(authority.host());
+ }
+ }
+ builder.build().unwrap_or_else(|_| uri.to_owned())
+}
+
+const ERROR_STATES: &[&str] = &[
+ "media-jam",
+ "toner-empty",
+ "spool-area-full",
+ "cover-open",
+ "door-open",
+ "input-tray-missing",
+ "output-tray-missing",
+ "marker-supply-empty",
+ "paused",
+ "shutdown",
+];
+
+/// Check if the printer is ready for printing
+///
+/// * `response` - IPP response to check
+pub fn is_printer_ready(response: &IppRequestResponse) -> Result<bool, IppError> {
+ let status = response.header().status_code();
+ if !status.is_success() {
+ return Err(IppError::StatusError(status));
+ }
+
+ let state = response
+ .attributes()
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .and_then(|g| g.attributes().get(IppAttribute::PRINTER_STATE))
+ .and_then(|attr| attr.value().as_enum())
+ .and_then(|v| PrinterState::from_i32(*v));
+
+ if let Some(PrinterState::Stopped) = state {
+ return Ok(false);
+ }
+
+ if let Some(reasons) = response
+ .attributes()
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .and_then(|g| g.attributes().get(IppAttribute::PRINTER_STATE_REASONS))
+ {
+ let keywords = reasons
+ .value()
+ .into_iter()
+ .filter_map(|e| e.as_keyword())
+ .map(ToOwned::to_owned)
+ .collect::<Vec<_>>();
+
+ if keywords.iter().any(|k| ERROR_STATES.contains(&&k[..])) {
+ return Ok(false);
+ }
+ }
+ Ok(true)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_canonicalize_uri() {
+ assert_eq!(
+ canonicalize_uri(&"http://user:pass@example.com:631/path?query=val".parse().unwrap()),
+ "ipp://example.com:631/path"
+ );
+ assert_eq!(
+ canonicalize_uri(&"http://example.com/path?query=val".parse().unwrap()),
+ "ipp://example.com/path"
+ );
+ }
+}
diff --git a/crates/ipp/src/value.rs b/crates/ipp/src/value.rs
new file mode 100644
index 0000000..2841c63
--- /dev/null
+++ b/crates/ipp/src/value.rs
@@ -0,0 +1,565 @@
+//!
+//! IPP value
+//!
+use std::{collections::BTreeMap, convert::Infallible, fmt, io, str::FromStr};
+
+use bytes::{Buf, BufMut, Bytes, BytesMut};
+use enum_as_inner::EnumAsInner;
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use crate::{model::ValueTag, FromPrimitive as _};
+
+#[inline]
+fn get_len_string(data: &mut Bytes) -> String {
+ let len = data.get_u16() as usize;
+ let s = String::from_utf8_lossy(&data[0..len]).into_owned();
+ data.advance(len);
+ s
+}
+
+/// IPP attribute values as defined in [RFC 8010](https://tools.ietf.org/html/rfc8010)
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumAsInner)]
+pub enum IppValue {
+ Integer(i32),
+ Enum(i32),
+ OctetString(String),
+ TextWithoutLanguage(String),
+ NameWithoutLanguage(String),
+ TextWithLanguage {
+ language: String,
+ text: String,
+ },
+ NameWithLanguage {
+ language: String,
+ name: String,
+ },
+ Charset(String),
+ NaturalLanguage(String),
+ Uri(String),
+ UriScheme(String),
+ RangeOfInteger {
+ min: i32,
+ max: i32,
+ },
+ Boolean(bool),
+ Keyword(String),
+ Array(Vec<IppValue>),
+ Collection(BTreeMap<String, IppValue>),
+ MimeMediaType(String),
+ DateTime {
+ year: u16,
+ month: u8,
+ day: u8,
+ hour: u8,
+ minutes: u8,
+ seconds: u8,
+ deci_seconds: u8,
+ utc_dir: char,
+ utc_hours: u8,
+ utc_mins: u8,
+ },
+ MemberAttrName(String),
+ Resolution {
+ cross_feed: i32,
+ feed: i32,
+ units: i8,
+ },
+ NoValue,
+ Other {
+ tag: u8,
+ data: Bytes,
+ },
+}
+
+impl IppValue {
+ /// Convert to binary tag
+ pub fn to_tag(&self) -> u8 {
+ match *self {
+ IppValue::Integer(_) => ValueTag::Integer as u8,
+ IppValue::Enum(_) => ValueTag::Enum as u8,
+ IppValue::RangeOfInteger { .. } => ValueTag::RangeOfInteger as u8,
+ IppValue::Boolean(_) => ValueTag::Boolean as u8,
+ IppValue::Keyword(_) => ValueTag::Keyword as u8,
+ IppValue::OctetString(_) => ValueTag::OctetStringUnspecified as u8,
+ IppValue::TextWithoutLanguage(_) => ValueTag::TextWithoutLanguage as u8,
+ IppValue::NameWithoutLanguage(_) => ValueTag::NameWithoutLanguage as u8,
+ IppValue::TextWithLanguage { .. } => ValueTag::TextWithLanguage as u8,
+ IppValue::NameWithLanguage { .. } => ValueTag::NameWithLanguage as u8,
+ IppValue::Charset(_) => ValueTag::Charset as u8,
+ IppValue::NaturalLanguage(_) => ValueTag::NaturalLanguage as u8,
+ IppValue::Uri(_) => ValueTag::Uri as u8,
+ IppValue::UriScheme(_) => ValueTag::UriScheme as u8,
+ IppValue::MimeMediaType(_) => ValueTag::MimeMediaType as u8,
+ IppValue::Array(ref array) => array.first().map(|v| v.to_tag()).unwrap_or(ValueTag::Unknown as u8),
+ IppValue::Collection(_) => ValueTag::BegCollection as u8,
+ IppValue::DateTime { .. } => ValueTag::DateTime as u8,
+ IppValue::MemberAttrName(_) => ValueTag::MemberAttrName as u8,
+ IppValue::Resolution { .. } => ValueTag::Resolution as u8,
+ IppValue::Other { tag, .. } => tag,
+ IppValue::NoValue => ValueTag::NoValue as u8,
+ }
+ }
+
+ /// Parse value from byte array which does not include the value length field
+ pub fn parse(value_tag: u8, mut data: Bytes) -> io::Result<IppValue> {
+ let ipp_tag = match ValueTag::from_u8(value_tag) {
+ Some(x) => x,
+ None => {
+ return Ok(IppValue::Other { tag: value_tag, data });
+ }
+ };
+
+ let value = match ipp_tag {
+ ValueTag::Integer => IppValue::Integer(data.get_i32()),
+ ValueTag::Enum => IppValue::Enum(data.get_i32()),
+ ValueTag::OctetStringUnspecified => IppValue::OctetString(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::TextWithoutLanguage => IppValue::TextWithoutLanguage(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::NameWithoutLanguage => IppValue::NameWithoutLanguage(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::TextWithLanguage => IppValue::TextWithLanguage {
+ language: get_len_string(&mut data),
+ text: get_len_string(&mut data),
+ },
+ ValueTag::NameWithLanguage => IppValue::NameWithLanguage {
+ language: get_len_string(&mut data),
+ name: get_len_string(&mut data),
+ },
+ ValueTag::Charset => IppValue::Charset(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::NaturalLanguage => IppValue::NaturalLanguage(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::Uri => IppValue::Uri(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::UriScheme => IppValue::UriScheme(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::RangeOfInteger => IppValue::RangeOfInteger {
+ min: data.get_i32(),
+ max: data.get_i32(),
+ },
+ ValueTag::Boolean => IppValue::Boolean(data.get_u8() != 0),
+ ValueTag::Keyword => IppValue::Keyword(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::MimeMediaType => IppValue::MimeMediaType(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::DateTime => IppValue::DateTime {
+ year: data.get_u16(),
+ month: data.get_u8(),
+ day: data.get_u8(),
+ hour: data.get_u8(),
+ minutes: data.get_u8(),
+ seconds: data.get_u8(),
+ deci_seconds: data.get_u8(),
+ utc_dir: data.get_u8() as char,
+ utc_hours: data.get_u8(),
+ utc_mins: data.get_u8(),
+ },
+ ValueTag::MemberAttrName => IppValue::MemberAttrName(String::from_utf8_lossy(&data).into_owned()),
+ ValueTag::Resolution => IppValue::Resolution {
+ cross_feed: data.get_i32(),
+ feed: data.get_i32(),
+ units: data.get_i8(),
+ },
+ ValueTag::NoValue => IppValue::NoValue,
+ _ => IppValue::Other { tag: value_tag, data },
+ };
+ Ok(value)
+ }
+
+ /// Write value to byte array, including leading value length field, excluding value tag
+ pub fn to_bytes(&self) -> Bytes {
+ let mut buffer = BytesMut::new();
+
+ match *self {
+ IppValue::Integer(i) | IppValue::Enum(i) => {
+ buffer.put_u16(4);
+ buffer.put_i32(i);
+ }
+ IppValue::RangeOfInteger { min, max } => {
+ buffer.put_u16(8);
+ buffer.put_i32(min);
+ buffer.put_i32(max);
+ }
+ IppValue::Boolean(b) => {
+ buffer.put_u16(1);
+ buffer.put_u8(b as u8);
+ }
+ IppValue::Keyword(ref s)
+ | IppValue::OctetString(ref s)
+ | IppValue::TextWithoutLanguage(ref s)
+ | IppValue::NameWithoutLanguage(ref s)
+ | IppValue::Charset(ref s)
+ | IppValue::NaturalLanguage(ref s)
+ | IppValue::Uri(ref s)
+ | IppValue::UriScheme(ref s)
+ | IppValue::MimeMediaType(ref s)
+ | IppValue::MemberAttrName(ref s) => {
+ buffer.put_u16(s.len() as u16);
+ buffer.put_slice(s.as_bytes());
+ }
+ IppValue::TextWithLanguage { ref language, ref text } => {
+ buffer.put_u16((language.len() + text.len() + 4) as u16);
+ buffer.put_u16(language.len() as u16);
+ buffer.put_slice(language.as_bytes());
+ buffer.put_u16(text.len() as u16);
+ buffer.put_slice(text.as_bytes());
+ }
+ IppValue::NameWithLanguage { ref language, ref name } => {
+ buffer.put_u16((language.len() + name.len() + 4) as u16);
+ buffer.put_u16(language.len() as u16);
+ buffer.put_slice(language.as_bytes());
+ buffer.put_u16(name.len() as u16);
+ buffer.put_slice(name.as_bytes());
+ }
+ IppValue::Array(ref list) => {
+ for (i, item) in list.iter().enumerate() {
+ buffer.put(item.to_bytes());
+ if i < list.len() - 1 {
+ buffer.put_u8(self.to_tag());
+ buffer.put_u16(0);
+ }
+ }
+ }
+ IppValue::Collection(ref list) => {
+ // begin collection: value size is 0
+ buffer.put_u16(0);
+
+ for item in list.iter() {
+ let atr_name = IppValue::MemberAttrName(item.0.to_string());
+ // item tag
+ buffer.put_u8(atr_name.to_tag());
+ // name size is zero, this is a collection
+ buffer.put_u16(0);
+
+ buffer.put(atr_name.to_bytes());
+
+ // item tag
+ buffer.put_u8(item.1.to_tag());
+ // name size is zero, this is a collection
+ buffer.put_u16(0);
+
+ buffer.put(item.1.to_bytes());
+ }
+ // write end collection attribute
+ buffer.put_u8(ValueTag::EndCollection as u8);
+ buffer.put_u32(0);
+ }
+ IppValue::DateTime {
+ year,
+ month,
+ day,
+ hour,
+ minutes,
+ seconds,
+ deci_seconds,
+ utc_dir,
+ utc_hours,
+ utc_mins,
+ } => {
+ buffer.put_u16(11);
+ buffer.put_u16(year);
+ buffer.put_u8(month);
+ buffer.put_u8(day);
+ buffer.put_u8(hour);
+ buffer.put_u8(minutes);
+ buffer.put_u8(seconds);
+ buffer.put_u8(deci_seconds);
+ buffer.put_u8(utc_dir as u8);
+ buffer.put_u8(utc_hours);
+ buffer.put_u8(utc_mins);
+ }
+ IppValue::Resolution {
+ cross_feed,
+ feed,
+ units,
+ } => {
+ buffer.put_u16(9);
+ buffer.put_i32(cross_feed);
+ buffer.put_i32(feed);
+ buffer.put_u8(units as u8);
+ }
+ IppValue::NoValue => buffer.put_u16(0),
+ IppValue::Other { ref data, .. } => {
+ buffer.put_u16(data.len() as u16);
+ buffer.put_slice(data);
+ }
+ }
+ buffer.freeze()
+ }
+}
+
+/// Implement Display trait to print the value
+impl fmt::Display for IppValue {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ IppValue::Integer(i) | IppValue::Enum(i) => write!(f, "{i}"),
+ IppValue::RangeOfInteger { min, max } => write!(f, "{min}..{max}"),
+ IppValue::Boolean(b) => write!(f, "{}", if b { "true" } else { "false" }),
+ IppValue::Keyword(ref s)
+ | IppValue::OctetString(ref s)
+ | IppValue::TextWithoutLanguage(ref s)
+ | IppValue::NameWithoutLanguage(ref s)
+ | IppValue::Charset(ref s)
+ | IppValue::NaturalLanguage(ref s)
+ | IppValue::Uri(ref s)
+ | IppValue::UriScheme(ref s)
+ | IppValue::MimeMediaType(ref s)
+ | IppValue::MemberAttrName(ref s) => write!(f, "{s}"),
+ IppValue::TextWithLanguage { ref language, ref text } => write!(f, "{language}:{text}"),
+ IppValue::NameWithLanguage { ref language, ref name } => write!(f, "{language}:{name}"),
+ IppValue::Array(ref array) => {
+ let s: Vec<String> = array.iter().map(|v| format!("{v}")).collect();
+ write!(f, "[{}]", s.join(", "))
+ }
+ IppValue::Collection(ref coll) => {
+ let s: Vec<String> = coll.iter().map(|(k, v)| format!("{k}={v}")).collect();
+ write!(f, "<{}>", s.join(", "))
+ }
+ IppValue::DateTime {
+ year,
+ month,
+ day,
+ hour,
+ minutes,
+ seconds,
+ deci_seconds,
+ utc_dir,
+ utc_hours,
+ ..
+ } => write!(
+ f,
+ "{year}-{month}-{day},{hour}:{minutes}:{seconds}.{deci_seconds},{utc_dir}{utc_hours}utc"
+ ),
+ IppValue::Resolution {
+ cross_feed,
+ feed,
+ units,
+ } => {
+ write!(f, "{cross_feed}x{feed}{}", if units == 3 { "in" } else { "cm" })
+ }
+
+ IppValue::NoValue => Ok(()),
+ IppValue::Other { tag, ref data } => write!(f, "{tag:0x}: {data:?}"),
+ }
+ }
+}
+
+impl FromStr for IppValue {
+ type Err = Infallible;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let value = match s {
+ "true" => IppValue::Boolean(true),
+ "false" => IppValue::Boolean(false),
+ other => {
+ if let Ok(iv) = other.parse::<i32>() {
+ IppValue::Integer(iv)
+ } else {
+ IppValue::Keyword(other.to_owned())
+ }
+ }
+ };
+ Ok(value)
+ }
+}
+
+impl<'a> IntoIterator for &'a IppValue {
+ type Item = &'a IppValue;
+ type IntoIter = IppValueIterator<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ IppValueIterator { value: self, index: 0 }
+ }
+}
+
+pub struct IppValueIterator<'a> {
+ value: &'a IppValue,
+ index: usize,
+}
+
+impl<'a> Iterator for IppValueIterator<'a> {
+ type Item = &'a IppValue;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ match self.value {
+ IppValue::Array(ref array) => {
+ if self.index < array.len() {
+ self.index += 1;
+ Some(&array[self.index - 1])
+ } else {
+ None
+ }
+ }
+ IppValue::Collection(ref map) => {
+ if let Some(entry) = map.iter().nth(self.index) {
+ self.index += 1;
+ Some(entry.1)
+ } else {
+ None
+ }
+ }
+ _ => {
+ if self.index == 0 {
+ self.index += 1;
+ Some(self.value)
+ } else {
+ None
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::collections::BTreeMap;
+
+ use crate::attribute::IppAttribute;
+ use crate::model::DelimiterTag;
+ use crate::parser::IppParser;
+ use crate::reader::IppReader;
+
+ use super::*;
+
+ fn value_check(value: IppValue) {
+ let mut b = value.to_bytes();
+ b.advance(2); // skip value size
+ assert_eq!(IppValue::parse(value.to_tag() as u8, b).unwrap(), value);
+ }
+
+ #[test]
+ fn test_value_single() {
+ value_check(IppValue::Integer(1234));
+ value_check(IppValue::Enum(4321));
+ value_check(IppValue::OctetString("octet-string".to_owned()));
+ value_check(IppValue::TextWithoutLanguage("text-without".to_owned()));
+ value_check(IppValue::NameWithoutLanguage("name-without".to_owned()));
+ value_check(IppValue::TextWithLanguage {
+ language: "en".to_owned(),
+ text: "text-with".to_owned(),
+ });
+ value_check(IppValue::NameWithLanguage {
+ language: "en".to_owned(),
+ name: "name-with".to_owned(),
+ });
+ value_check(IppValue::Charset("charset".to_owned()));
+ value_check(IppValue::NaturalLanguage("natural".to_owned()));
+ value_check(IppValue::Uri("uri".to_owned()));
+ value_check(IppValue::UriScheme("urischeme".to_owned()));
+ value_check(IppValue::RangeOfInteger { min: -12, max: 45 });
+ value_check(IppValue::Boolean(true));
+ value_check(IppValue::Boolean(false));
+ value_check(IppValue::Keyword("keyword".to_owned()));
+ value_check(IppValue::MimeMediaType("mime".to_owned()));
+ value_check(IppValue::DateTime {
+ year: 2020,
+ month: 2,
+ day: 13,
+ hour: 12,
+ minutes: 34,
+ seconds: 22,
+ deci_seconds: 1,
+ utc_dir: 'c',
+ utc_hours: 1,
+ utc_mins: 30,
+ });
+ value_check(IppValue::MemberAttrName("member".to_owned()));
+ value_check(IppValue::Resolution {
+ cross_feed: 800,
+ feed: 600,
+ units: 2,
+ });
+ value_check(IppValue::NoValue);
+ value_check(IppValue::Other {
+ tag: 123,
+ data: "foo".into(),
+ });
+ }
+
+ #[test]
+ fn test_value_iterator_single() {
+ let val = IppValue::Integer(1234);
+
+ for v in &val {
+ assert_eq!(*v, val);
+ }
+ }
+
+ #[test]
+ fn test_value_iterator_multiple() {
+ let list = vec![IppValue::Integer(1234), IppValue::Integer(5678)];
+ let val = IppValue::Array(list.clone());
+
+ for v in val.into_iter().enumerate() {
+ assert_eq!(*v.1, list[v.0]);
+ }
+ }
+
+ #[test]
+ fn test_array() {
+ let attr = IppAttribute::new(
+ "list",
+ IppValue::Array(vec![IppValue::Integer(0x1111_1111), IppValue::Integer(0x2222_2222)]),
+ );
+ let buf = attr.to_bytes().to_vec();
+
+ assert_eq!(
+ buf,
+ vec![
+ 0x21, 0, 4, b'l', b'i', b's', b't', 0, 4, 0x11, 0x11, 0x11, 0x11, 0x21, 0, 0, 0, 4, 0x22, 0x22, 0x22,
+ 0x22
+ ],
+ );
+
+ let mut data = vec![1, 1, 0, 0, 0, 0, 0, 0, 4];
+ data.extend(buf);
+ data.push(3);
+
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse();
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("list").unwrap();
+ assert_eq!(
+ attr.value().as_array(),
+ Some(&vec![IppValue::Integer(0x1111_1111), IppValue::Integer(0x2222_2222)])
+ );
+ }
+
+ #[test]
+ fn test_collection() {
+ let attr = IppAttribute::new(
+ "coll",
+ IppValue::Collection(BTreeMap::from([("abcd".to_string(), IppValue::Integer(0x2222_2222))])),
+ );
+ let buf = attr.to_bytes();
+
+ assert_eq!(
+ vec![
+ 0x34, 0, 4, b'c', b'o', b'l', b'l', 0, 0, 0x4a, 0, 0, 0, 4, b'a', b'b', b'c', b'd', 0x21, 0, 0, 0, 4,
+ 0x22, 0x22, 0x22, 0x22, 0x37, 0, 0, 0, 0,
+ ],
+ buf
+ );
+
+ let mut data = vec![1, 1, 0, 0, 0, 0, 0, 0, 4];
+ data.extend(buf);
+ data.push(3);
+
+ let result = IppParser::new(IppReader::new(io::Cursor::new(data))).parse();
+ assert!(result.is_ok());
+
+ let res = result.ok().unwrap();
+ let attrs = res
+ .attributes
+ .groups_of(DelimiterTag::PrinterAttributes)
+ .next()
+ .unwrap()
+ .attributes();
+ let attr = attrs.get("coll").unwrap();
+ assert_eq!(
+ attr.value(),
+ &IppValue::Collection(BTreeMap::from([("abcd".to_string(), IppValue::Integer(0x2222_2222))]))
+ );
+ }
+}
diff --git a/pseudo_crate/Cargo.lock b/pseudo_crate/Cargo.lock
index 5724eda..7f90d27 100644
--- a/pseudo_crate/Cargo.lock
+++ b/pseudo_crate/Cargo.lock
@@ -249,10 +249,10 @@
"hex",
"hound",
"http 0.2.12",
- "http-body",
+ "http-body 0.4.6",
"httparse",
"httpdate",
- "hyper",
+ "hyper 0.14.32",
"hyper-timeout",
"icu_capi",
"icu_casemap",
@@ -273,6 +273,7 @@
"inotify-sys",
"intrusive-collections",
"ipnet",
+ "ipp",
"is-terminal",
"itertools 0.14.0",
"itoa",
@@ -476,7 +477,7 @@
"toml_datetime",
"toml_edit 0.22.20",
"tonic",
- "tower",
+ "tower 0.4.13",
"tower-layer",
"tower-service",
"tracing",
@@ -800,8 +801,8 @@
"bytes",
"futures-util",
"http 0.2.12",
- "http-body",
- "hyper",
+ "http-body 0.4.6",
+ "hyper 0.14.32",
"itoa",
"matchit",
"memchr",
@@ -815,7 +816,7 @@
"serde_urlencoded",
"sync_wrapper 0.1.2",
"tokio",
- "tower",
+ "tower 0.4.13",
"tower-layer",
"tower-service",
]
@@ -830,7 +831,7 @@
"bytes",
"futures-util",
"http 0.2.12",
- "http-body",
+ "http-body 0.4.6",
"mime",
"rustversion",
"tower-layer",
@@ -2840,6 +2841,29 @@
]
[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http 1.1.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "pin-project-lite",
+]
+
+[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2869,7 +2893,7 @@
"futures-util",
"h2 0.3.26",
"http 0.2.12",
- "http-body",
+ "http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
@@ -2882,18 +2906,72 @@
]
[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
name = "hyper-timeout"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
dependencies = [
- "hyper",
+ "hyper 0.14.32",
"pin-project-lite",
"tokio",
"tokio-io-timeout",
]
[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper 1.6.0",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "hyper 1.6.0",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3510,6 +3588,27 @@
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
+name = "ipp"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a74ba7383ea538b5c356323681ec5f4208eedf502e4e13886c82cf65af636ca"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "enum-as-inner",
+ "enum-primitive-derive",
+ "futures-executor",
+ "futures-util",
+ "http 1.1.0",
+ "log",
+ "native-tls",
+ "num-traits",
+ "reqwest",
+ "thiserror 2.0.11",
+ "tokio-util",
+]
+
+[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4263,6 +4362,23 @@
]
[[package]]
+name = "native-tls"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
+dependencies = [
+ "libc 0.2.161",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4532,6 +4648,12 @@
]
[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
name = "openssl-sys"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5340,6 +5462,48 @@
]
[[package]]
+name = "reqwest"
+version = "0.12.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.6.0",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.2",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower 0.5.2",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "windows-registry",
+]
+
+[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5463,6 +5627,21 @@
]
[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
+
+[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5539,6 +5718,15 @@
]
[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5563,6 +5751,29 @@
]
[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.9.0",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc 0.2.161",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc 0.2.161",
+]
+
+[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6051,6 +6262,9 @@
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
[[package]]
name = "synstructure"
@@ -6340,6 +6554,16 @@
]
[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
name = "tokio-openssl"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6382,6 +6606,7 @@
dependencies = [
"bytes",
"futures-core",
+ "futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@@ -6465,15 +6690,15 @@
"bytes",
"h2 0.3.26",
"http 0.2.12",
- "http-body",
- "hyper",
+ "http-body 0.4.6",
+ "hyper 0.14.32",
"hyper-timeout",
"percent-encoding",
"pin-project",
"prost",
"tokio",
"tokio-stream",
- "tower",
+ "tower 0.4.13",
"tower-layer",
"tower-service",
"tracing",
@@ -6500,6 +6725,21 @@
]
[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper 1.0.2",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7117,6 +7357,18 @@
]
[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7146,6 +7398,19 @@
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
+name = "wasm-streams"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
name = "wayland-backend"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7287,6 +7552,36 @@
]
[[package]]
+name = "windows-registry"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
+dependencies = [
+ "windows-result",
+ "windows-strings",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+dependencies = [
+ "windows-result",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/pseudo_crate/Cargo.toml b/pseudo_crate/Cargo.toml
index 0c43dcf..99224d5 100644
--- a/pseudo_crate/Cargo.toml
+++ b/pseudo_crate/Cargo.toml
@@ -183,6 +183,7 @@
inotify-sys = "=0.1.5"
intrusive-collections = "=0.9.7"
ipnet = "=2.11.0"
+ipp = "=5.2.0"
is-terminal = "=0.4.16"
itertools = "=0.14.0"
itoa = "=1.0.15"
diff --git a/pseudo_crate/crate-list.txt b/pseudo_crate/crate-list.txt
index 2224c03..7a0c031 100644
--- a/pseudo_crate/crate-list.txt
+++ b/pseudo_crate/crate-list.txt
@@ -176,6 +176,7 @@
instant
intrusive-collections
ipnet
+ipp
is-terminal
itertools
itoa