[go: nahoru, domu]

Directly construct IPP requests for print job management

Instead of relying on libcups functions to do it for us. This allows us
to bypass the awkward abstraction of cups_option_t, where all attributes
had to be encoded as strings, and send collection-type attributes like
"client-info" without relying on the undocumented internal format used
by libcups.

Bug: b:267349303
Test: send print jobs with various options to ippeveprinter
Test: print something using an actual printer
Change-Id: I7ec030c28e33dbf9a68cfbb14185c585b3a797b9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4209858
Reviewed-by: Benjamin Gordon <bmgordon@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Sparik Hayrapetyan <ust@google.com>
Auto-Submit: Bryan Cain <bryancain@chromium.org>
Commit-Queue: Bryan Cain <bryancain@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1103416}
diff --git a/printing/backend/cups_ipp_constants.cc b/printing/backend/cups_ipp_constants.cc
index 56c15dc..5f03904 100644
--- a/printing/backend/cups_ipp_constants.cc
+++ b/printing/backend/cups_ipp_constants.cc
@@ -10,15 +10,24 @@
 
 namespace printing {
 
+// operation attributes
+constexpr char kIppDocumentFormat[] = "document-format";         // RFC 8011
+constexpr char kIppDocumentName[] = "document-name";             // RFC 8011
+constexpr char kIppJobId[] = "job-id";                           // RFC 8011
+constexpr char kIppJobName[] = "job-name";                       // RFC 8011
+constexpr char kIppLastDocument[] = "last-document";             // RFC 8011
+constexpr char kIppPin[] = "job-password";                       // PWG 5100.11
+constexpr char kIppPinEncryption[] = "job-password-encryption";  // PWG 5100.11
+constexpr char kIppPrinterUri[] = "printer-uri";                 // RFC 8011
+constexpr char kIppRequestingUserName[] = "requesting-user-name";  // RFC 8011
+
+// job attributes
 constexpr char kIppCollate[] = "multiple-document-handling";  // PWG 5100.19
 constexpr char kIppCopies[] = CUPS_COPIES;
 constexpr char kIppColor[] = CUPS_PRINT_COLOR_MODE;
 constexpr char kIppMedia[] = CUPS_MEDIA;
 constexpr char kIppDuplex[] = CUPS_SIDES;
-constexpr char kIppResolution[] = "printer-resolution";            // RFC 8011
-constexpr char kIppRequestingUserName[] = "requesting-user-name";  // RFC 8011
-constexpr char kIppPin[] = "job-password";                       // PWG 5100.11
-constexpr char kIppPinEncryption[] = "job-password-encryption";  // PWG 5100.11
+constexpr char kIppResolution[] = "printer-resolution";  // RFC 8011
 
 // collation values
 constexpr char kCollated[] = "separate-documents-collated-copies";
diff --git a/printing/backend/cups_ipp_constants.h b/printing/backend/cups_ipp_constants.h
index ba5575e..f107c02 100644
--- a/printing/backend/cups_ipp_constants.h
+++ b/printing/backend/cups_ipp_constants.h
@@ -10,16 +10,24 @@
 
 namespace printing {
 
-// property names
+// operation attributes
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppDocumentFormat[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppDocumentName[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppJobId[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppJobName[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppLastDocument[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppPin[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppPinEncryption[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppPrinterUri[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppRequestingUserName[];
+
+// job attributes
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppCollate[];
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppCopies[];
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppColor[];
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppMedia[];
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppDuplex[];
-COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppRequestingUserName[];
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppResolution[];
-COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppPin[];
-COMPONENT_EXPORT(PRINT_BACKEND) extern const char kIppPinEncryption[];
 
 // collation values
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kCollated[];
diff --git a/printing/backend/cups_printer.cc b/printing/backend/cups_printer.cc
index 5f7dfddd..1e0707c 100644
--- a/printing/backend/cups_printer.cc
+++ b/printing/backend/cups_printer.cc
@@ -15,9 +15,11 @@
 #include "build/build_config.h"
 #include "printing/backend/cups_connection.h"
 #include "printing/backend/cups_ipp_constants.h"
+#include "printing/backend/cups_ipp_helper.h"
 #include "printing/backend/print_backend.h"
 #include "printing/backend/print_backend_consts.h"
 #include "printing/print_job_constants.h"
+#include "url/gurl.h"
 
 namespace printing {
 
@@ -27,6 +29,11 @@
       : cups_http_(http), destination_(std::move(dest)) {
     DCHECK(cups_http_);
     DCHECK(destination_);
+
+    printer_uri_ = cupsGetOption(kCUPSOptPrinterUriSupported,
+                                 destination_.get()->num_options,
+                                 destination_.get()->options);
+    resource_path_ = std::string(GURL(printer_uri_).path_piece());
   }
 
   CupsPrinterImpl(const CupsPrinterImpl&) = delete;
@@ -172,40 +179,56 @@
   ipp_status_t CreateJob(int* job_id,
                          const std::string& title,
                          const std::string& username,
-                         const std::vector<cups_option_t>& options) override {
-    DCHECK(dest_info_) << "Verify availability before starting a print job";
+                         ipp_t* attributes) override {
+    ScopedIppPtr request = CreateRequest(IPP_OP_CREATE_JOB, username);
 
-    cups_option_t* data = const_cast<cups_option_t*>(
-        options.data());  // createDestJob will not modify the data
-    if (!username.empty())
-      cupsSetUser(username.c_str());
+    if (!title.empty()) {
+      ippAddString(request.get(), IPP_TAG_OPERATION, IPP_TAG_NAME, kIppJobName,
+                   nullptr, title.c_str());
+    }
 
-    ipp_status_t create_status = cupsCreateDestJob(
-        cups_http_, destination_.get(), dest_info_.get(), job_id,
-        title.empty() ? nullptr : title.c_str(), options.size(), data);
-    cupsSetUser(nullptr);  // reset to default username ("anonymous")
-    return create_status;
+    CopyAttributeGroup(request.get(), attributes, IPP_TAG_OPERATION);
+    CopyAttributeGroup(request.get(), attributes, IPP_TAG_JOB);
+    // We would also copy subscription attributes here if we actually used
+    // any. We don't, though.
+
+    // cupsDoRequest() takes ownership of the request and frees it for us.
+    ScopedIppPtr response = WrapIpp(
+        cupsDoRequest(cups_http_, request.release(), resource_path_.c_str()));
+
+    ipp_attribute_t* attr =
+        ippFindAttribute(response.get(), kIppJobId, IPP_TAG_INTEGER);
+    *job_id = ippGetInteger(attr, 0);
+
+    return ippGetStatusCode(response.get());
   }
 
   bool StartDocument(int job_id,
                      const std::string& docname,
                      bool last_document,
                      const std::string& username,
-                     const std::vector<cups_option_t>& options) override {
-    DCHECK(dest_info_);
+                     ipp_t* attributes) override {
     DCHECK(job_id);
-    if (!username.empty())
-      cupsSetUser(username.c_str());
+    ScopedIppPtr request = CreateRequest(IPP_OP_SEND_DOCUMENT, username);
 
-    cups_option_t* data = const_cast<cups_option_t*>(
-        options.data());  // createStartDestDocument will not modify the data
-    http_status_t start_doc_status = cupsStartDestDocument(
-        cups_http_, destination_.get(), dest_info_.get(), job_id,
-        docname.empty() ? nullptr : docname.c_str(), CUPS_FORMAT_PDF,
-        options.size(), data, last_document ? 1 : 0);
+    ippAddInteger(request.get(), IPP_TAG_OPERATION, IPP_TAG_INTEGER, kIppJobId,
+                  job_id);
+    ippAddBoolean(request.get(), IPP_TAG_OPERATION, kIppLastDocument,
+                  static_cast<char>(last_document));
+    ippAddString(request.get(), IPP_TAG_OPERATION, IPP_TAG_MIMETYPE,
+                 kIppDocumentFormat, nullptr, CUPS_FORMAT_PDF);
+    if (!docname.empty()) {
+      ippAddString(request.get(), IPP_TAG_OPERATION, IPP_TAG_NAME,
+                   kIppDocumentName, nullptr, docname.c_str());
+    }
 
-    cupsSetUser(nullptr);  // reset to default username ("anonymous")
-    return start_doc_status == HTTP_CONTINUE;
+    CopyAttributeGroup(request.get(), attributes, IPP_TAG_OPERATION);
+    CopyAttributeGroup(request.get(), attributes, IPP_TAG_DOCUMENT);
+
+    http_status_t status =
+        cupsSendRequest(cups_http_, request.get(), resource_path_.c_str(),
+                        CUPS_LENGTH_VARIABLE);
+    return status == HTTP_CONTINUE;
   }
 
   bool StreamData(const std::vector<char>& buffer) override {
@@ -215,24 +238,22 @@
   }
 
   bool FinishDocument() override {
-    DCHECK(dest_info_);
-
-    ipp_status_t status = cupsFinishDestDocument(cups_http_, destination_.get(),
-                                                 dest_info_.get());
-
+    ScopedIppPtr response =
+        WrapIpp(cupsGetResponse(cups_http_, resource_path_.c_str()));
+    ipp_status_t status = ippGetStatusCode(response.get());
     return status == IPP_STATUS_OK;
   }
 
   ipp_status_t CloseJob(int job_id, const std::string& username) override {
-    DCHECK(dest_info_);
     DCHECK(job_id);
-    if (!username.empty())
-      cupsSetUser(username.c_str());
+    ScopedIppPtr request = CreateRequest(IPP_OP_CLOSE_JOB, username);
 
-    ipp_status_t result = cupsCloseDestJob(cups_http_, destination_.get(),
-                                           dest_info_.get(), job_id);
-    cupsSetUser(nullptr);  // reset to default username ("anonymous")
-    return result;
+    ippAddInteger(request.get(), IPP_TAG_OPERATION, IPP_TAG_INTEGER, kIppJobId,
+                  job_id);
+
+    ScopedIppPtr response = WrapIpp(
+        cupsDoRequest(cups_http_, request.release(), resource_path_.c_str()));
+    return ippGetStatusCode(response.get());
   }
 
   bool CancelJob(int job_id) override {
@@ -258,6 +279,29 @@
   }
 
  private:
+  // internal helper function to initialize an IPP request
+  ScopedIppPtr CreateRequest(ipp_op_t op, const std::string& username) {
+    const char* c_username = username.empty() ? cupsUser() : username.c_str();
+
+    ipp_t* request = ippNewRequest(op);
+    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, kIppPrinterUri,
+                 nullptr, printer_uri_.c_str());
+    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
+                 kIppRequestingUserName, nullptr, c_username);
+
+    return WrapIpp(request);
+  }
+
+  // internal helper function to copy attributes to an IPP request
+  void CopyAttributeGroup(ipp_t* request, ipp_t* attributes, ipp_tag_t group) {
+    for (ipp_attribute_t* attr = ippFirstAttribute(attributes); attr;
+         attr = ippNextAttribute(attributes)) {
+      if (ippGetGroupTag(attr) == group) {
+        ippCopyAttribute(request, attr, 0);
+      }
+    }
+  }
+
   // http connection owned by the CupsConnection which created this object
   const raw_ptr<http_t> cups_http_;
 
@@ -266,6 +310,12 @@
 
   // opaque object containing printer attributes and options
   mutable ScopedDestInfo dest_info_;
+
+  // uri used to connect to this printer
+  std::string printer_uri_;
+
+  // resource path used to connect to this printer
+  std::string resource_path_;
 };
 
 std::unique_ptr<CupsPrinter> CupsPrinter::Create(http_t* http,
diff --git a/printing/backend/cups_printer.h b/printing/backend/cups_printer.h
index 0f2f30ef..b273b529 100644
--- a/printing/backend/cups_printer.h
+++ b/printing/backend/cups_printer.h
@@ -94,18 +94,19 @@
   virtual ipp_status_t CreateJob(int* job_id,
                                  const std::string& title,
                                  const std::string& username,
-                                 const std::vector<cups_option_t>& options) = 0;
+                                 ipp_t* attributes) = 0;
 
   // Add a document to a print job.  `job_id` must be non-zero and refer to a
   // job started with CreateJob.  `docname` will be displayed in print status
   // if not empty.  `last_doc` should be true if this is the last document for
-  // this print job.  `username` is not sent if empty.  `options` should be IPP
-  // key value pairs for the Send-Document operation.
+  // this print job.  `username` is not sent if empty.  `attributes` should
+  // contain IPP operation and document attributes for the Send-Document
+  // operation.
   virtual bool StartDocument(int job_id,
                              const std::string& docname,
                              bool last_doc,
                              const std::string& username,
-                             const std::vector<cups_option_t>& options) = 0;
+                             ipp_t* attributes) = 0;
 
   // Add data to the current document started by StartDocument.  Calling this
   // without a started document will fail.
diff --git a/printing/backend/mock_cups_printer.h b/printing/backend/mock_cups_printer.h
index f5f478d1..504d97a 100644
--- a/printing/backend/mock_cups_printer.h
+++ b/printing/backend/mock_cups_printer.h
@@ -27,13 +27,13 @@
                ipp_status_t(int* job_id,
                             const std::string& title,
                             const std::string& username,
-                            const std::vector<cups_option_t>& options));
+                            ipp_t* attributes));
   MOCK_METHOD5(StartDocument,
                bool(int job_id,
                     const std::string& docname,
                     bool last_doc,
                     const std::string& username,
-                    const std::vector<cups_option_t>& options));
+                    ipp_t* attributes));
   MOCK_METHOD1(StreamData, bool(const std::vector<char>& buffer));
   MOCK_METHOD0(FinishDocument, bool());
   MOCK_METHOD2(CloseJob, ipp_status_t(int job_id, const std::string& username));
diff --git a/printing/backend/print_backend_consts.cc b/printing/backend/print_backend_consts.cc
index 7c1d139..2127b77 100644
--- a/printing/backend/print_backend_consts.cc
+++ b/printing/backend/print_backend_consts.cc
@@ -25,3 +25,4 @@
 const char kCUPSOptPrinterMakeAndModel[] = "printer-make-and-model";
 const char kCUPSOptPrinterState[] = "printer-state";
 const char kCUPSOptPrinterType[] = "printer-type";
+const char kCUPSOptPrinterUriSupported[] = "printer-uri-supported";
diff --git a/printing/backend/print_backend_consts.h b/printing/backend/print_backend_consts.h
index 3cfd3f47..bf4957b 100644
--- a/printing/backend/print_backend_consts.h
+++ b/printing/backend/print_backend_consts.h
@@ -24,5 +24,6 @@
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kCUPSOptPrinterMakeAndModel[];
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kCUPSOptPrinterState[];
 COMPONENT_EXPORT(PRINT_BACKEND) extern const char kCUPSOptPrinterType[];
+COMPONENT_EXPORT(PRINT_BACKEND) extern const char kCUPSOptPrinterUriSupported[];
 
 #endif  // PRINTING_BACKEND_PRINT_BACKEND_CONSTS_H_
diff --git a/printing/client_info_helpers.cc b/printing/client_info_helpers.cc
index 5c5ac8c..f68951b 100644
--- a/printing/client_info_helpers.cc
+++ b/printing/client_info_helpers.cc
@@ -5,8 +5,6 @@
 #include "printing/client_info_helpers.h"
 
 #include "base/no_destructor.h"
-#include "base/strings/strcat.h"
-#include "base/strings/stringprintf.h"
 #include "base/types/optional_util.h"
 #include "printing/mojom/print.mojom.h"
 #include "third_party/re2/src/re2/re2.h"
@@ -26,11 +24,8 @@
          (value->size() <= max_length && RE2::FullMatch(*value, *kStringRegex));
 }
 
-// Returns true if all members of `client_info` are valid.
-// String members are considered valid if they match the regex [a-zA-Z0-9_.-]*
-// and do not exceed the maximum length specified for the respective IPP member
-// attribute. The `client_type` member is valid if it is equal to one of the
-// enum values defined for the `client-type` IPP attribute.
+}  // namespace
+
 bool ValidateClientInfoItem(const mojom::IppClientInfo& client_info) {
   return ValidateClientType(client_info.client_type) &&
          ValidateStringMember(&client_info.client_name,
@@ -43,39 +38,4 @@
                               kClientInfoMaxVersionLength);
 }
 
-}  // namespace
-
-absl::optional<std::string> ClientInfoCollectionToCupsOptionValue(
-    const mojom::IppClientInfo& client_info) {
-  if (!ValidateClientInfoItem(client_info)) {
-    return absl::nullopt;
-  }
-  std::string name = base::StrCat({"client-name=", client_info.client_name});
-  std::string type = base::StringPrintf(
-      "client-type=%d", static_cast<int>(client_info.client_type));
-  std::string string_version = base::StrCat(
-      {"client-string-version=", client_info.client_string_version});
-
-  // Missing values for 'client-version' and 'client-patches' correspond to
-  // 'no-value' out-of-band IPP values. We omit them because there is no
-  // string encoding as a cups_option_t for them that CUPS understands.
-  std::string version;
-  if (client_info.client_version.has_value()) {
-    version =
-        base::StrCat({"client-version=", client_info.client_version.value()});
-  }
-  std::string patches;
-  if (client_info.client_patches.has_value()) {
-    patches =
-        base::StrCat({"client-patches=", client_info.client_patches.value()});
-  }
-
-  // The resulting string may have extra spaces between attributes because
-  // of missing member attributes which is okay because they are ignored
-  // by cupsParseOptions.
-  return base::StringPrintf("{%s %s %s %s %s}", name.c_str(), type.c_str(),
-                            version.c_str(), string_version.c_str(),
-                            patches.c_str());
-}
-
 }  // namespace printing
diff --git a/printing/client_info_helpers.h b/printing/client_info_helpers.h
index 3c306ade..df96eea 100644
--- a/printing/client_info_helpers.h
+++ b/printing/client_info_helpers.h
@@ -5,11 +5,9 @@
 #ifndef PRINTING_CLIENT_INFO_HELPERS_H_
 #define PRINTING_CLIENT_INFO_HELPERS_H_
 
-#include <string>
-
+#include <cstddef>
 #include "base/component_export.h"
 #include "printing/mojom/print.mojom-forward.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace printing {
 
@@ -19,15 +17,13 @@
 inline constexpr size_t kClientInfoMaxStringVersionLength = 127;
 inline constexpr size_t kClientInfoMaxVersionLength = 64;
 
-// Returns the string representation of `client_info` in a format suitable for
-// use as a `cups_option_t` value, or absl::nullopt if `client_info` is invalid.
-// `client_info` represents one value of the 'client-info' multi-valued IPP
-// attribute. `client_info` is considered valid if all string members match the
-// regex [a-zA-Z0-9_.-]* and do not exceed the maximum length specified for the
-// respective IPP member attribute.
+// Returns true if all members of `client_info` are valid.
+// String members are considered valid if they match the regex [a-zA-Z0-9_.-]*
+// and do not exceed the maximum length specified for the respective IPP member
+// attribute. The `client_type` member is valid if it is equal to one of the
+// enum values defined for the `client-type` IPP attribute.
 COMPONENT_EXPORT(PRINTING)
-absl::optional<std::string> ClientInfoCollectionToCupsOptionValue(
-    const mojom::IppClientInfo& client_info);
+bool ValidateClientInfoItem(const mojom::IppClientInfo& client_info);
 
 }  // namespace printing
 
diff --git a/printing/client_info_helpers_unittest.cc b/printing/client_info_helpers_unittest.cc
index fd834dc..2f28fa3 100644
--- a/printing/client_info_helpers_unittest.cc
+++ b/printing/client_info_helpers_unittest.cc
@@ -17,60 +17,24 @@
 
 namespace {
 
-TEST(ClientInfoHelpersTest,
-     ClientInfoCollectionToCupsOptionValueValidWithAllFields) {
+TEST(ClientInfoHelpersTest, ValidateClientInfoItemValidWithAllFields) {
   mojom::IppClientInfo client_info(
       mojom::IppClientInfo::ClientType::kOperatingSystem, "a-", "B_", "1.",
       "a.1-B_");
 
-  absl::optional<std::string> option_val =
-      ClientInfoCollectionToCupsOptionValue(client_info);
-  ASSERT_TRUE(option_val.has_value());
-  ASSERT_GE(option_val.value().size(), 2u);
-  ASSERT_EQ(option_val.value().front(), '{');
-  ASSERT_EQ(option_val.value().back(), '}');
-
-  base::StringPiece option_without_braces(option_val.value());
-  option_without_braces.remove_prefix(1);
-  option_without_braces.remove_suffix(1);
-
-  std::vector<std::string> member_options =
-      base::SplitString(option_without_braces, " ", base::TRIM_WHITESPACE,
-                        base::SPLIT_WANT_NONEMPTY);
-
-  EXPECT_THAT(member_options,
-              testing::UnorderedElementsAre(
-                  "client-name=a-", "client-type=4", "client-patches=B_",
-                  "client-string-version=1.", "client-version=a.1-B_"));
+  EXPECT_TRUE(ValidateClientInfoItem(client_info));
 }
 
-TEST(ClientInfoHelpersTest,
-     ClientInfoCollectionToCupsOptionValueValidWithMissingFields) {
+TEST(ClientInfoHelpersTest, ValidateClientInfoItemValidWithMissingFields) {
   mojom::IppClientInfo::ClientType type =
       mojom::IppClientInfo::ClientType::kApplication;
   mojom::IppClientInfo client_info(type, "a-", absl::nullopt, "1.",
                                    absl::nullopt);
-  absl::optional<std::string> option_val =
-      ClientInfoCollectionToCupsOptionValue(client_info);
-  ASSERT_TRUE(option_val.has_value());
-  ASSERT_GE(option_val.value().size(), 2u);
-  ASSERT_EQ(option_val.value().front(), '{');
-  ASSERT_EQ(option_val.value().back(), '}');
 
-  base::StringPiece option_without_braces(option_val.value());
-  option_without_braces.remove_prefix(1);
-  option_without_braces.remove_suffix(1);
-
-  std::vector<std::string> member_options =
-      base::SplitString(option_without_braces, " ", base::TRIM_WHITESPACE,
-                        base::SPLIT_WANT_NONEMPTY);
-
-  EXPECT_THAT(member_options,
-              testing::UnorderedElementsAre("client-name=a-", "client-type=3",
-                                            "client-string-version=1."));
+  EXPECT_TRUE(ValidateClientInfoItem(client_info));
 }
 
-TEST(ClientInfoHelpersTest, ClientInfoCollectionToCupsOptionValueInvalidChars) {
+TEST(ClientInfoHelpersTest, ValidateClientInfoItemInvalidChars) {
   mojom::IppClientInfo valid_client_info(
       mojom::IppClientInfo::ClientType::kOther, "name", "patch", "version",
       absl::nullopt);
@@ -79,22 +43,22 @@
 
   client_info = valid_client_info;
   client_info.client_name = " ";
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_patches = ";";
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_version = "\\";
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_string_version = "{";
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 }
 
-TEST(ClientInfoHelpersTest, ClientInfoCollectionToCupsOptionValueInvalidRange) {
+TEST(ClientInfoHelpersTest, ValidateClientInfoItemInvalidRange) {
   mojom::IppClientInfo valid_client_info(
       mojom::IppClientInfo::ClientType::kOther, "name", "patch", "version",
       absl::nullopt);
@@ -103,32 +67,32 @@
 
   client_info = valid_client_info;
   client_info.client_name = std::string(kClientInfoMaxNameLength + 1, 'A');
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_patches =
       std::string(kClientInfoMaxPatchesLength + 1, 'A');
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_version =
       std::string(kClientInfoMaxVersionLength + 1, 'A');
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_string_version =
       std::string(kClientInfoMaxStringVersionLength + 1, 'A');
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_type = static_cast<mojom::IppClientInfo::ClientType>(
       static_cast<int>(mojom::IppClientInfo::ClientType::kMinValue) - 1);
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 
   client_info = valid_client_info;
   client_info.client_type = static_cast<mojom::IppClientInfo::ClientType>(
       static_cast<int>(mojom::IppClientInfo::ClientType::kMaxValue) + 1);
-  EXPECT_FALSE(ClientInfoCollectionToCupsOptionValue(client_info).has_value());
+  EXPECT_FALSE(ValidateClientInfoItem(client_info));
 }
 
 }  // namespace
diff --git a/printing/print_settings_conversion_chromeos_unittest.cc b/printing/print_settings_conversion_chromeos_unittest.cc
index 43abdb6..92c4a2e6 100644
--- a/printing/print_settings_conversion_chromeos_unittest.cc
+++ b/printing/print_settings_conversion_chromeos_unittest.cc
@@ -4,25 +4,15 @@
 
 #include "printing/print_settings_conversion_chromeos.h"
 
-#include <ostream>
 #include <string>
 
 #include "base/test/values_test_util.h"
 #include "base/values.h"
-#include "printing/client_info_helpers.h"
 #include "printing/mojom/print.mojom.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace printing {
 
-namespace mojom {
-std::ostream& operator<<(std::ostream& os, const IppClientInfo& value) {
-  absl::optional<std::string> str =
-      ClientInfoCollectionToCupsOptionValue(value);
-  return os << (str.has_value() ? str.value() : std::string(""));
-}
-}  // namespace mojom
-
 namespace {
 
 const base::Value::List kClientInfoJobSetting = base::test::ParseJsonList(R"([
diff --git a/printing/printing_context_chromeos.cc b/printing/printing_context_chromeos.cc
index 796358eb..68b0f869 100644
--- a/printing/printing_context_chromeos.cc
+++ b/printing/printing_context_chromeos.cc
@@ -46,37 +46,54 @@
          base::StartsWith(uri, "usb:") || base::StartsWith(uri, "ippusb:");
 }
 
-// Returns the string representation of `client_infos` in a format suitable to
-// be used as a `cups_option_t` value.
-// `client_infos` represents the 'client-info' IPP attribute. Each item in
-// `client_infos` represents one collection in 'client-info'.
+// Populates the 'client-info' attribute of the IPP collection `options`. Each
+// item in `client_infos` represents one collection in 'client-info'.
 // Invalid 'client-info' items will be dropped.
-std::string ClientInfoToCupsOptionValue(
-    const std::vector<mojom::IppClientInfo>& client_infos) {
-  // String representation for each client-info item.
-  std::vector<std::string> option_values;
+void EncodeClientInfo(const std::vector<mojom::IppClientInfo>& client_infos,
+                      ipp_t* options) {
+  std::vector<ScopedIppPtr> option_values;
+  std::vector<const ipp_t*> raw_option_values;
   option_values.reserve(client_infos.size());
+  raw_option_values.reserve(client_infos.size());
+
   for (const mojom::IppClientInfo& client_info : client_infos) {
-    if (auto option_value =
-            ClientInfoCollectionToCupsOptionValue(client_info)) {
-      option_values.emplace_back(option_value.value());
-    } else {
+    if (!ValidateClientInfoItem(client_info)) {
       LOG(WARNING) << "Invalid client-info item skipped";
+      continue;
+    }
+
+    // Create a temporary collection object owned by this function.
+    ipp_t* collection = ippNew();
+    option_values.emplace_back(WrapIpp(collection));
+    raw_option_values.emplace_back(collection);
+
+    ippAddString(collection, IPP_TAG_ZERO, IPP_TAG_NAME, kIppClientName,
+                 nullptr, client_info.client_name.c_str());
+    ippAddInteger(collection, IPP_TAG_ZERO, IPP_TAG_ENUM, kIppClientType,
+                  static_cast<int>(client_info.client_type));
+    ippAddString(collection, IPP_TAG_ZERO, IPP_TAG_TEXT,
+                 kIppClientStringVersion, nullptr,
+                 client_info.client_string_version.c_str());
+
+    if (client_info.client_version.has_value()) {
+      ippAddOctetString(collection, IPP_TAG_ZERO, kIppClientVersion,
+                        client_info.client_version.value().data(),
+                        client_info.client_version.value().size());
+    }
+
+    if (client_info.client_patches.has_value()) {
+      ippAddString(collection, IPP_TAG_ZERO, IPP_TAG_TEXT, kIppClientPatches,
+                   nullptr, client_info.client_patches.value().c_str());
     }
   }
-  return base::JoinString(option_values, ",");
-}
 
-ScopedCupsOption ConstructOption(std::string name, std::string value) {
-  // ScopedCupsOption frees the name and value buffers on deletion
-  cups_option_t* cups_option = nullptr;
-  int num_options = 0;
-  // Use cupsAddOption so that the pair of malloc and free are used.
-  num_options =
-      cupsAddOption(name.c_str(), value.c_str(), num_options, &cups_option);
-  DCHECK(cups_option);
-  DCHECK_EQ(num_options, 1);
-  return ScopedCupsOption(cups_option);
+  if (raw_option_values.empty()) {
+    return;
+  }
+
+  // Now add the client-info list to the options.
+  ippAddCollections(options, IPP_TAG_OPERATION, kIppClientInfo,
+                    raw_option_values.size(), raw_option_values.data());
 }
 
 std::string GetCollateString(bool collate) {
@@ -137,8 +154,10 @@
 
 }  // namespace
 
-std::vector<ScopedCupsOption> SettingsToCupsOptions(
-    const PrintSettings& settings) {
+ScopedIppPtr SettingsToIPPOptions(const PrintSettings& settings) {
+  ScopedIppPtr scoped_options = WrapIpp(ippNew());
+  ipp_t* options = scoped_options.get();
+
   const char* sides = nullptr;
   switch (settings.duplex_mode()) {
     case mojom::DuplexMode::kSimplex:
@@ -154,33 +173,36 @@
       NOTREACHED();
   }
 
-  std::vector<ScopedCupsOption> options;
-  options.push_back(
-      ConstructOption(kIppColor,
-                      GetIppColorModelForModel(settings.color())));  // color
-  options.push_back(ConstructOption(kIppDuplex, sides));  // duplexing
-  options.push_back(
-      ConstructOption(kIppMedia,
-                      settings.requested_media().vendor_id));  // paper size
-  options.push_back(
-      ConstructOption(kIppCopies,
-                      base::NumberToString(settings.copies())));  // copies
-  options.push_back(
-      ConstructOption(kIppCollate,
-                      GetCollateString(settings.collate())));  // collate
+  // duplexing
+  ippAddString(options, IPP_TAG_JOB, IPP_TAG_KEYWORD, kIppDuplex, nullptr,
+               sides);
+  // color
+  ippAddString(options, IPP_TAG_JOB, IPP_TAG_KEYWORD, kIppColor, nullptr,
+               GetIppColorModelForModel(settings.color()).c_str());
+  // paper size
+  ippAddString(options, IPP_TAG_JOB, IPP_TAG_KEYWORD, kIppMedia, nullptr,
+               settings.requested_media().vendor_id.c_str());
+  // copies
+  ippAddInteger(options, IPP_TAG_JOB, IPP_TAG_INTEGER, kIppCopies,
+                settings.copies());
+  // collate
+  ippAddString(options, IPP_TAG_JOB, IPP_TAG_KEYWORD, kIppCollate, nullptr,
+               GetCollateString(settings.collate()).c_str());
+
   if (!settings.pin_value().empty()) {
-    options.push_back(ConstructOption(kIppPin, settings.pin_value()));
-    options.push_back(ConstructOption(kIppPinEncryption, kPinEncryptionNone));
+    ippAddOctetString(options, IPP_TAG_OPERATION, kIppPin,
+                      settings.pin_value().data(), settings.pin_value().size());
+    ippAddString(options, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, kIppPinEncryption,
+                 nullptr, kPinEncryptionNone);
   }
 
+  // resolution
   if (settings.dpi_horizontal() > 0 && settings.dpi_vertical() > 0) {
-    std::string dpi = base::NumberToString(settings.dpi_horizontal());
-    if (settings.dpi_horizontal() != settings.dpi_vertical())
-      dpi += "x" + base::NumberToString(settings.dpi_vertical());
-    options.push_back(ConstructOption(kIppResolution, dpi + "dpi"));
+    ippAddResolution(options, IPP_TAG_JOB, kIppResolution, IPP_RES_PER_INCH,
+                     settings.dpi_horizontal(), settings.dpi_vertical());
   }
 
-  std::map<std::string, std::vector<std::string>> multival;
+  std::map<std::string, std::vector<int>> multival;
   for (const auto& setting : settings.advanced_settings()) {
     const std::string& key = setting.first;
     const std::string& value = setting.second.GetString();
@@ -191,36 +213,39 @@
     size_t pos = key.find('/');
     if (pos == std::string::npos) {
       // Regular value.
-      options.push_back(ConstructOption(key, value));
+      ippAddString(options, IPP_TAG_JOB, IPP_TAG_KEYWORD, key.c_str(), nullptr,
+                   value.c_str());
       continue;
     }
     // Store selected enum values.
-    if (value == kOptionTrue)
-      multival[key.substr(0, pos)].push_back(key.substr(pos + 1));
+    if (value == kOptionTrue) {
+      std::string option_name = key.substr(0, pos);
+      std::string enum_string = key.substr(pos + 1);
+      int enum_value = ippEnumValue(option_name.c_str(), enum_string.c_str());
+      DCHECK_NE(enum_value, -1);
+      multival[option_name].push_back(enum_value);
+    }
   }
 
-  // Pass multivalue enums as comma-separated lists.
+  // Add multivalue enum options.
   for (const auto& it : multival) {
-    options.push_back(
-        ConstructOption(it.first, base::JoinString(it.second, ",")));
+    ippAddIntegers(options, IPP_TAG_JOB, IPP_TAG_ENUM, it.first.c_str(),
+                   it.second.size(), it.second.data());
   }
 
   // OAuth access token
   if (!settings.oauth_token().empty()) {
-    options.push_back(ConstructOption(kSettingChromeOSAccessOAuthToken,
-                                      settings.oauth_token()));
+    ippAddString(options, IPP_TAG_JOB, IPP_TAG_NAME,
+                 kSettingChromeOSAccessOAuthToken, nullptr,
+                 settings.oauth_token().c_str());
   }
 
   // IPP client-info attribute.
   if (!settings.client_infos().empty()) {
-    std::string option_value =
-        ClientInfoToCupsOptionValue(settings.client_infos());
-    if (!option_value.empty()) {
-      options.push_back(ConstructOption(kIppClientInfo, option_value));
-    }
+    EncodeClientInfo(settings.client_infos(), options);
   }
 
-  return options;
+  return scoped_options;
 }
 
 // static
@@ -247,12 +272,15 @@
 
 PrintingContextChromeos::PrintingContextChromeos(Delegate* delegate)
     : PrintingContext(delegate),
-      connection_(CupsConnection::Create(GURL(), HTTP_ENCRYPT_NEVER, true)) {}
+      connection_(CupsConnection::Create(GURL(), HTTP_ENCRYPT_NEVER, true)),
+      ipp_options_(WrapIpp(nullptr)) {}
 
 PrintingContextChromeos::PrintingContextChromeos(
     Delegate* delegate,
     std::unique_ptr<CupsConnection> connection)
-    : PrintingContext(delegate), connection_(std::move(connection)) {}
+    : PrintingContext(delegate),
+      connection_(std::move(connection)),
+      ipp_options_(WrapIpp(nullptr)) {}
 
 PrintingContextChromeos::~PrintingContextChromeos() {
   ReleaseContext();
@@ -362,7 +390,7 @@
   CupsPrinter::CupsMediaMargins margins =
       printer_->GetMediaMarginsByName(media.vendor_id);
   SetPrintableArea(settings_.get(), media, margins);
-  cups_options_ = SettingsToCupsOptions(*settings_);
+  ipp_options_ = SettingsToIPPOptions(*settings_);
   send_user_info_ = settings_->send_user_info();
   if (send_user_info_) {
     DCHECK(printer_);
@@ -404,18 +432,8 @@
                          : kDocumentNamePlaceholder;
   }
 
-  std::vector<cups_option_t> options;
-  for (const ScopedCupsOption& option : cups_options_) {
-    if (printer_->CheckOptionSupported(option->name, option->value)) {
-      options.push_back(*(option.get()));
-    } else {
-      DVLOG(1) << "Unsupported option skipped " << option->name << ", "
-               << option->value;
-    }
-  }
-
-  ipp_status_t create_status =
-      printer_->CreateJob(&job_id_, converted_name, username_, options);
+  ipp_status_t create_status = printer_->CreateJob(
+      &job_id_, converted_name, username_, ipp_options_.get());
 
   if (job_id_ == 0) {
     DLOG(WARNING) << "Creating cups job failed"
@@ -425,7 +443,7 @@
 
   // we only send one document, so it's always the last one
   if (!printer_->StartDocument(job_id_, converted_name, true, username_,
-                               options)) {
+                               ipp_options_.get())) {
     LOG(ERROR) << "Starting document failed";
     return OnError();
   }
diff --git a/printing/printing_context_chromeos.h b/printing/printing_context_chromeos.h
index d31180c..4950f9f 100644
--- a/printing/printing_context_chromeos.h
+++ b/printing/printing_context_chromeos.h
@@ -10,7 +10,7 @@
 #include <vector>
 
 #include "printing/backend/cups_connection.h"
-#include "printing/backend/cups_deleters.h"
+#include "printing/backend/cups_ipp_helper.h"
 #include "printing/backend/cups_printer.h"
 #include "printing/mojom/print.mojom.h"
 #include "printing/printing_context.h"
@@ -59,7 +59,7 @@
 
   const std::unique_ptr<CupsConnection> connection_;
   std::unique_ptr<CupsPrinter> printer_;
-  std::vector<ScopedCupsOption> cups_options_;
+  ScopedIppPtr ipp_options_;
   bool send_user_info_ = false;
   std::string username_;
 };
@@ -67,8 +67,7 @@
 // This has the side effect of recording UMA for advanced attributes usage,
 // so only call once per job.
 COMPONENT_EXPORT(PRINTING)
-std::vector<ScopedCupsOption> SettingsToCupsOptions(
-    const PrintSettings& settings);
+ScopedIppPtr SettingsToIPPOptions(const PrintSettings& settings);
 
 }  // namespace printing
 
diff --git a/printing/printing_context_chromeos_unittest.cc b/printing/printing_context_chromeos_unittest.cc
index a6e1346..7654413 100644
--- a/printing/printing_context_chromeos_unittest.cc
+++ b/printing/printing_context_chromeos_unittest.cc
@@ -78,34 +78,69 @@
     printing_context_->UpdatePrintSettingsFromPOD(std::move(settings));
   }
 
-  void TestCupsOptionValue(const char* option_name,
-                           const char* expected_option_value) const {
-    DCHECK(option_name);
-    auto cups_options = SettingsToCupsOptions(settings_);
-    const char* ret = nullptr;
-    for (const auto& option : cups_options) {
-      EXPECT_TRUE(option->name);
-      EXPECT_TRUE(option->value);
-      if (option->name && !strcmp(option_name, option->name)) {
+  ipp_attribute_t* GetAttribute(ipp_t* attributes,
+                                const char* attr_name) const {
+    DCHECK(attr_name);
+    ipp_attribute_t* ret = nullptr;
+    for (ipp_attribute_t* attr = ippFirstAttribute(attributes); attr;
+         attr = ippNextAttribute(attributes)) {
+      const char* name = ippGetName(attr);
+      if (name && !strcmp(attr_name, name)) {
         EXPECT_EQ(nullptr, ret)
-            << "Multiple options with name " << option_name << " found.";
-        ret = option->value;
+            << "Multiple attributes with name " << attr_name << " found.";
+        ret = attr;
       }
     }
-    EXPECT_STREQ(expected_option_value, ret);
+    EXPECT_TRUE(ret);
+    return ret;
   }
 
-  absl::optional<std::string> GetCupsOptionValue(
-      const char* option_name) const {
-    DCHECK(option_name);
-    auto cups_options = SettingsToCupsOptions(settings_);
-    absl::optional<std::string> ret;
-    for (const auto& option : cups_options) {
-      if (option->name && !strcmp(option_name, option->name)) {
-        ret = std::string(option->value);
-      }
-    }
-    return ret;
+  void TestStringOptionValue(const char* attr_name,
+                             const char* expected_value) const {
+    auto attributes = SettingsToIPPOptions(settings_);
+    auto* attr = GetAttribute(attributes.get(), attr_name);
+    EXPECT_STREQ(expected_value, ippGetString(attr, 0, nullptr));
+  }
+
+  void TestIntegerOptionValue(const char* attr_name, int expected_value) const {
+    auto attributes = SettingsToIPPOptions(settings_);
+    auto* attr = GetAttribute(attributes.get(), attr_name);
+    EXPECT_EQ(expected_value, ippGetInteger(attr, 0));
+  }
+
+  void TestOctetStringOptionValue(const char* attr_name,
+                                  base::span<const char> expected_value) const {
+    auto attributes = SettingsToIPPOptions(settings_);
+    auto* attr = GetAttribute(attributes.get(), attr_name);
+    int length;
+    void* value = ippGetOctetString(attr, 0, &length);
+    ASSERT_EQ(expected_value.size(), static_cast<size_t>(length));
+    ASSERT_TRUE(value);
+    EXPECT_EQ(0, memcmp(expected_value.data(), value, expected_value.size()));
+  }
+
+  void TestResolutionOptionValue(const char* attr_name,
+                                 int expected_x_res,
+                                 int expected_y_res) const {
+    auto attributes = SettingsToIPPOptions(settings_);
+    auto* attr = GetAttribute(attributes.get(), attr_name);
+    ipp_res_t unit;
+    int y_res;
+    int x_res = ippGetResolution(attr, 0, &y_res, &unit);
+    EXPECT_EQ(unit, IPP_RES_PER_INCH);
+    EXPECT_EQ(expected_x_res, x_res);
+    EXPECT_EQ(expected_y_res, y_res);
+  }
+
+  bool HasAttribute(const char* attr_name) const {
+    auto attributes = SettingsToIPPOptions(settings_);
+    return !!ippFindAttribute(attributes.get(), attr_name, IPP_TAG_ZERO);
+  }
+
+  int GetAttrValueCount(const char* attr_name) const {
+    auto attributes = SettingsToIPPOptions(settings_);
+    auto* attr = GetAttribute(attributes.get(), attr_name);
+    return ippGetCount(attr);
   }
 
   TestPrintSettings settings_;
@@ -118,59 +153,59 @@
   raw_ptr<MockCupsPrinter> printer_;
 };
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_Color) {
+TEST_F(PrintingContextTest, SettingsToIPPOptions_Color) {
   settings_.set_color(mojom::ColorModel::kGray);
-  TestCupsOptionValue(kIppColor, "monochrome");
+  TestStringOptionValue(kIppColor, "monochrome");
   settings_.set_color(mojom::ColorModel::kColor);
-  TestCupsOptionValue(kIppColor, "color");
+  TestStringOptionValue(kIppColor, "color");
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_Duplex) {
+TEST_F(PrintingContextTest, SettingsToIPPOptions_Duplex) {
   settings_.set_duplex_mode(mojom::DuplexMode::kSimplex);
-  TestCupsOptionValue(kIppDuplex, "one-sided");
+  TestStringOptionValue(kIppDuplex, "one-sided");
   settings_.set_duplex_mode(mojom::DuplexMode::kLongEdge);
-  TestCupsOptionValue(kIppDuplex, "two-sided-long-edge");
+  TestStringOptionValue(kIppDuplex, "two-sided-long-edge");
   settings_.set_duplex_mode(mojom::DuplexMode::kShortEdge);
-  TestCupsOptionValue(kIppDuplex, "two-sided-short-edge");
+  TestStringOptionValue(kIppDuplex, "two-sided-short-edge");
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_Media) {
-  TestCupsOptionValue(kIppMedia, "");
+TEST_F(PrintingContextTest, SettingsToIPPOptions_Media) {
+  TestStringOptionValue(kIppMedia, "");
   settings_.set_requested_media(
       {gfx::Size(297000, 420000), "iso_a3_297x420mm"});
-  TestCupsOptionValue(kIppMedia, "iso_a3_297x420mm");
+  TestStringOptionValue(kIppMedia, "iso_a3_297x420mm");
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_Copies) {
+TEST_F(PrintingContextTest, SettingsToIPPOptions_Copies) {
   settings_.set_copies(3);
-  TestCupsOptionValue(kIppCopies, "3");
+  TestIntegerOptionValue(kIppCopies, 3);
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_Collate) {
-  TestCupsOptionValue(kIppCollate, "separate-documents-uncollated-copies");
+TEST_F(PrintingContextTest, SettingsToIPPOptions_Collate) {
+  TestStringOptionValue(kIppCollate, "separate-documents-uncollated-copies");
   settings_.set_collate(true);
-  TestCupsOptionValue(kIppCollate, "separate-documents-collated-copies");
+  TestStringOptionValue(kIppCollate, "separate-documents-collated-copies");
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_Pin) {
-  TestCupsOptionValue(kIppPin, nullptr);
+TEST_F(PrintingContextTest, SettingsToIPPOptions_Pin) {
+  EXPECT_FALSE(HasAttribute(kIppPin));
   settings_.set_pin_value("1234");
-  TestCupsOptionValue(kIppPin, "1234");
+  TestOctetStringOptionValue(kIppPin, base::make_span("1234", 4u));
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_Resolution) {
-  TestCupsOptionValue(kIppResolution, nullptr);
+TEST_F(PrintingContextTest, SettingsToIPPOptions_Resolution) {
+  EXPECT_FALSE(HasAttribute(kIppResolution));
   settings_.set_dpi_xy(0, 300);
-  TestCupsOptionValue(kIppResolution, nullptr);
+  EXPECT_FALSE(HasAttribute(kIppResolution));
   settings_.set_dpi_xy(300, 0);
-  TestCupsOptionValue(kIppResolution, nullptr);
+  EXPECT_FALSE(HasAttribute(kIppResolution));
   settings_.set_dpi(600);
-  TestCupsOptionValue(kIppResolution, "600dpi");
+  TestResolutionOptionValue(kIppResolution, 600, 600);
   settings_.set_dpi_xy(600, 1200);
-  TestCupsOptionValue(kIppResolution, "600x1200dpi");
+  TestResolutionOptionValue(kIppResolution, 600, 1200);
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_SendUserInfo_Secure) {
+TEST_F(PrintingContextTest, SettingsToIPPOptions_SendUserInfo_Secure) {
   ipp_status_t status = ipp_status_t::IPP_STATUS_OK;
   std::u16string document_name = kDocumentName16;
   SetDefaultSettings(/*send_user_info=*/true, "ipps://test-uri");
@@ -194,7 +229,7 @@
   EXPECT_EQ(start_document_username, kUsername);
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_SendUserInfo_Insecure) {
+TEST_F(PrintingContextTest, SettingsToIPPOptions_SendUserInfo_Insecure) {
   ipp_status_t status = ipp_status_t::IPP_STATUS_OK;
   std::u16string document_name = kDocumentName16;
   std::string default_username = "chronos";
@@ -220,7 +255,7 @@
   EXPECT_EQ(start_document_username, default_username);
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptions_DoNotSendUserInfo) {
+TEST_F(PrintingContextTest, SettingsToIPPOptions_DoNotSendUserInfo) {
   ipp_status_t status = ipp_status_t::IPP_STATUS_OK;
   std::u16string document_name = kDocumentName16;
   SetDefaultSettings(/*send_user_info=*/false, "ipps://test-uri");
@@ -244,7 +279,41 @@
   EXPECT_EQ(start_document_username, "");
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptionsClientInfo) {
+TEST_F(PrintingContextTest, SettingsToIPPOptionsClientInfo) {
+  mojom::IppClientInfo client_info(
+      mojom::IppClientInfo::ClientType::kOperatingSystem, "a-", "B_", "1.",
+      "a.1-B_");
+  settings_.set_client_infos({client_info});
+
+  auto attributes = SettingsToIPPOptions(settings_);
+  auto* attr = ippFindAttribute(attributes.get(), kIppClientInfo,
+                                IPP_TAG_BEGIN_COLLECTION);
+  auto* client_info_collection = ippGetCollection(attr, 0);
+
+  attr = ippFindAttribute(client_info_collection, kIppClientName, IPP_TAG_NAME);
+  EXPECT_STREQ("a-", ippGetString(attr, 0, nullptr));
+
+  attr = ippFindAttribute(client_info_collection, kIppClientType, IPP_TAG_ENUM);
+  EXPECT_EQ(4, ippGetInteger(attr, 0));
+
+  attr =
+      ippFindAttribute(client_info_collection, kIppClientPatches, IPP_TAG_TEXT);
+  EXPECT_STREQ("B_", ippGetString(attr, 0, nullptr));
+
+  attr = ippFindAttribute(client_info_collection, kIppClientStringVersion,
+                          IPP_TAG_TEXT);
+  EXPECT_STREQ("1.", ippGetString(attr, 0, nullptr));
+
+  attr = ippFindAttribute(client_info_collection, kIppClientVersion,
+                          IPP_TAG_STRING);
+  int length;
+  void* version = ippGetOctetString(attr, 0, &length);
+  ASSERT_TRUE(version);
+  EXPECT_EQ(6, length);
+  EXPECT_EQ(0, memcmp("a.1-B_", version, 6));
+}
+
+TEST_F(PrintingContextTest, SettingsToIPPOptionsClientInfoSomeValid) {
   mojom::IppClientInfo valid_client_info(
       mojom::IppClientInfo::ClientType::kOperatingSystem, "aB.1-_", "aB.1-_",
       "aB.1-_", "aB.1-_");
@@ -253,28 +322,22 @@
       "aB.1-_", "aB.1-_");
   settings_.set_client_infos(
       {valid_client_info, invalid_client_info, valid_client_info});
-  absl::optional<std::string> option_val = GetCupsOptionValue(kIppClientInfo);
-  ASSERT_TRUE(option_val.has_value());
 
-  // Check that the invalid item is skipped in the CUPS option string.
-  size_t client_info_item_count =
-      base::SplitString(option_val.value(), ",", base::KEEP_WHITESPACE,
-                        base::SPLIT_WANT_ALL)
-          .size();
-  EXPECT_EQ(client_info_item_count, 2u);
+  // Check that the invalid item is skipped in the client-info collection.
+  EXPECT_EQ(GetAttrValueCount(kIppClientInfo), 2);
 }
 
-TEST_F(PrintingContextTest, SettingsToCupsOptionsClientInfoEmpty) {
+TEST_F(PrintingContextTest, SettingsToIPPOptionsClientInfoEmpty) {
   settings_.set_client_infos({});
-  absl::optional<std::string> option_val = GetCupsOptionValue(kIppClientInfo);
-  EXPECT_FALSE(option_val.has_value());
+  EXPECT_FALSE(HasAttribute(kIppClientInfo));
 
   mojom::IppClientInfo invalid_client_info(
       mojom::IppClientInfo::ClientType::kOther, "$", " ", "{}", absl::nullopt);
 
   settings_.set_client_infos({invalid_client_info});
-  EXPECT_FALSE(GetCupsOptionValue(kIppClientInfo).has_value());
+  EXPECT_FALSE(HasAttribute(kIppClientInfo));
 }
+
 }  // namespace
 
 }  // namespace printing