Add PPD Pin support for Ricoh Printers

Adds ability for complex remapping of IPP->PPD values.

BUG=chromium:1082308
TEST=tast run printer.PinPrint*

Change-Id: Ieeca1c13dac5f7f951b99c3de01979c525099891
diff --git a/cups/ppd-cache.c b/cups/ppd-cache.c
index 194cf91..9b57363 100644
--- a/cups/ppd-cache.c
+++ b/cups/ppd-cache.c
@@ -60,6 +60,8 @@
 static int      pwg_find_size_tracker(const char *media_name,
                                       const struct pwg_media_size_tracker_s *trackers,
                                       int trackers_length);
+static char *   _ppdGetAttributeValue(ipp_attribute_t *attr);
+static char *   _ppdTransformValue(ipp_t *collection, char *value_buf);
 
 
 /*
@@ -4416,6 +4418,234 @@
 
 
 /*
+ * '_ppdGetAttributeValue()' retrieves the value from attr and converts it to a
+ *  string. Caller is responsible for freeing the returned string when done.
+ */
+static char *               /* O - allocated buffer containing the value */
+_ppdGetAttributeValue(
+  ipp_attribute_t *attr)    /* I - attribute whose value should be retrieved */
+{
+  char *value_buf = NULL;
+  int  value_buf_size;
+
+
+  switch (ippGetValueTag(attr))
+  {
+    case IPP_TAG_INTEGER:
+    {
+      int value = ippGetInteger(attr, 0);
+      value_buf_size = snprintf(NULL, 0, "%d", value);
+      if ((value_buf = (char *)calloc((size_t)++value_buf_size,
+                                      sizeof(char))) == NULL)
+        break;
+      snprintf(value_buf, (size_t)value_buf_size, "%d", value);
+    }
+    break;
+
+    case IPP_TAG_BOOLEAN:
+      {
+        const char *value = ippGetBoolean(attr, 0) ? "True" : "False";
+        value_buf_size = strlen(value);
+        if ((value_buf = (char *)calloc((size_t)++value_buf_size,
+                                        sizeof(char))) == NULL)
+          break;
+        strlcpy(value_buf, value, (size_t)value_buf_size);
+      }
+      break;
+
+    case IPP_TAG_STRING:
+      {
+        char *ptr;
+        if ((ptr = ippGetOctetString(attr, 0, &value_buf_size)) != NULL)
+        {
+          if ((value_buf = (char *)calloc((size_t)(value_buf_size + 1),
+                                          sizeof(char))) != NULL)
+          {
+            strncat(value_buf, ptr, (size_t)value_buf_size);
+          }
+        }
+      }
+      break;
+
+    default:
+      {
+        // See if the value can be decoded as a string. If not, it is not
+        // supported..
+        const char* string;
+        if ((string = ippGetString(attr, 0, NULL)) != NULL)
+        {
+          value_buf_size = strlen(string);
+          if ((value_buf = (char *)calloc((size_t)++value_buf_size,
+                                          sizeof(char))) == NULL)
+            break;
+          strlcpy(value_buf, string, (size_t)value_buf_size);
+        }
+        else
+        {
+          DEBUG_printf((
+              "1_ppdGetAttributeValue: Unsupported attr: name=%s, value_tag=%d",
+              ippGetName(attr), ippGetValueTag(attr)));
+        }
+      }
+      break;
+  }
+
+  return value_buf;
+}
+
+/*
+ * '_ppdTransformValue()' - Applies any necessary transformations to the IPP
+ *  value to make it acceptable to the PPD. Any attribute in |collection| that
+ *  begins with '_' is considered to be a transformation attribute, the name
+ *  indicating the type of transformation. This function will take ownership of
+ *  |value_buf|, but the caller is responsible for the lifecycle of the return
+ *  value.
+ */
+static char *                  /* O - allocated buffer containing the value */
+_ppdTransformValue(
+  ipp_t *collection,           /* I - transformations to perform */
+  char  *value_buf)            /* I - buffer to be transformed. Not valid
+                                *     after call completes. */
+{
+  ipp_attribute_t *option;
+
+
+  for (option = ippFirstAttribute(collection);
+       option;
+       option = ippNextAttribute(collection))
+  {
+    const char *option_name = ippGetName(option);
+
+
+    // Skip the actual option attributes.
+    if (option_name[0] != '_')
+      continue;
+
+    if (!strcmp(option_name, "_delimiters"))
+    {
+      const char *delimiters = ippGetString(option, 0, NULL);
+      char       *p = strpbrk(value_buf, delimiters);
+
+      if (p)
+        *p = '\0';
+    }
+    else if (!strcmp(option_name, "_constraint"))
+    {
+      int max,
+          min = ippGetRange(option, 0, &max);
+
+
+      if (strlen(value_buf) < min)
+      {
+        // If the supplied value is too small, then we can't convert this value,
+        // so don't pass it to the filters.
+        DEBUG_printf(("1_ppdTransformValue: %s is shorter than %d", value_buf,
+                      min));
+        free (value_buf);
+        return NULL;
+      }
+      else if (strlen(value_buf) > max)
+      {
+        // Truncate the value at the length allowed.
+        value_buf[max] = '\0';
+      }
+    }
+    else if (!strcmp(option_name, "_valid-chars"))
+    {
+      ipp_attribute_t *replacement_attr;
+      const char      *replacement_char;
+
+
+      if (((replacement_attr = ippFindAttribute(collection,
+                                                "_replacement-char",
+                                                IPP_TAG_KEYWORD)) != NULL) &&
+          ((replacement_char = ippGetString(replacement_attr, 0,
+                                            NULL)) != NULL) &&
+          (strlen(replacement_char) == 1))
+      {
+        const char *valid_chars = ippGetString(option, 0, NULL);
+        int         i,
+                    len = strlen(value_buf);
+
+
+        for (i = 0; i < len; ++i)
+        {
+          if (strchr(valid_chars, value_buf[i]) == NULL)
+          {
+            // Invalid character. Replace it with the replacement char.
+            value_buf[i] = replacement_char[0];
+          }
+        }
+      }
+      else
+      {
+        DEBUG_printf(
+            ("1_ppdTransformValue: Can't apply _valid_chars attribute because "
+             "no _replacement-char found"));
+        free(value_buf);
+        return NULL;
+      }
+    }
+    else if (!strcmp(option_name, "_replacement-char"))
+    {
+      // Just skip this one - it's handled in _valid-chars above.
+      continue;
+    }
+    else if (!strcmp(option_name, "_mapping"))
+    {
+      /*
+       * Collection of the IPP -> PPD value mappings.
+       */
+      ipp_t *enum_collection = ippGetCollection(option, 0);
+      if (enum_collection)
+      {
+        const char      *option_val;
+        ipp_attribute_t *option_attr;
+
+
+        if (((option_attr = ippFindAttribute(enum_collection, value_buf,
+                                             IPP_TAG_KEYWORD)) != NULL) &&
+            ((option_val = ippGetString(option_attr, 0, NULL)) != NULL)) {
+          /*
+           * Replace the IPP string with the PPD string.
+           */
+          size_t new_size = (strlen(option_val) + 1) * sizeof(char);
+          if ((value_buf = (char *)realloc(value_buf, new_size)) == NULL)
+          {
+            free(value_buf);
+            return NULL;
+          }
+          strcpy(value_buf, option_val);
+        }
+        else
+        {
+          ipp_attribute_t *enum_attr;
+          DEBUG_printf(("1_ppdTransformValue: No mapping found for %s",
+                        value_buf));
+
+          free(value_buf);
+          return NULL;
+        }
+      } else {
+        DEBUG_printf(
+            ("1_ppdTransformValue: No collection found in option: name=%s, "
+             "value_tag=%d", ippGetName(option), ippGetValueTag(option)));
+        free(value_buf);
+        return NULL;
+      }
+    }
+    else
+    {
+      DEBUG_printf(
+          ("1_ppdTransformValue: unknown transformation. attr: name=%s, "
+           "value_tag=%d", ippGetName(option), ippGetValueTag(option)));
+    }
+  }
+
+  return value_buf;
+}
+
+/*
  * '_ppdConvertOptions()' - Converts the relevant ipp attributes found in
  *  |job_attrs| to PPD options suitable for sending to a filter, using
  *  |option_mappings| to determine which attribute should be converted, and to
@@ -4455,84 +4685,29 @@
                                     ippGetName(attr),
                                     IPP_TAG_BEGIN_COLLECTION)) != NULL)
     {
-      char *value_buf = NULL;
-      int  value_buf_size;
-
-
       /*
        * Since a mapping was found for this IPP attribute, convert the value
        * of the attribute to a string. This is so it can be added to the mapping
        * option values below. Do this IPP value -> string conversion only once,
        * though, since the mapping may contain several OEM-specific options.
        */
+      char            *value_buf;
+      ipp_attribute_t *option;
+      ipp_t           *collection = ippGetCollection(mapping, 0);
+
+
+      if ((value_buf = _ppdGetAttributeValue(attr)) == NULL)
+        continue;
+
+      DEBUG_printf(("3_ppdConvertOptions: attr(%s)=%s",ippGetName(attr),
+                    value_buf));
+
+      if ((value_buf = _ppdTransformValue(collection, value_buf)) == NULL)
+        continue;
+
       DEBUG_printf(
-          ("3_ppdConvertOptions: Found mapping for attr: name=%s, value_tag=%d",
-           ippGetName(attr), ippGetValueTag(attr)));
-      switch (ippGetValueTag(attr))
-      {
-        case IPP_TAG_INTEGER:
-        {
-          int value = ippGetInteger(attr, 0);
-          value_buf_size = snprintf(NULL, 0, "%d", value);
-          if ((value_buf = (char *)calloc((size_t)++value_buf_size,
-                                          sizeof(char))) == NULL)
-            continue;
-          snprintf(value_buf, (size_t)value_buf_size, "%d", value);
-        }
-        break;
-
-        case IPP_TAG_BOOLEAN:
-          {
-            const char *value = ippGetBoolean(attr, 0) ? "True" : "False";
-            value_buf_size = strlen(value);
-            if ((value_buf = (char *)calloc((size_t)++value_buf_size,
-                                            sizeof(char))) == NULL)
-              continue;
-            strlcpy(value_buf, value, (size_t)value_buf_size);
-          }
-          break;
-
-        case IPP_TAG_STRING:
-          {
-            char *ptr;
-            if ((ptr = ippGetOctetString(attr, 0, &value_buf_size)) != NULL) {
-              if ((value_buf = (char *)calloc((size_t)(value_buf_size + 1),
-                                              sizeof(char))) != NULL) {
-                strncat(value_buf, ptr, (size_t)value_buf_size);
-              }
-            }
-          }
-          break;
-
-        default:
-          {
-            // See if the value can be decoded as a string. If not, it is not
-            // supported..
-            const char* string;
-            if ((string = ippGetString(attr, 0, NULL)) != NULL) {
-              value_buf_size = strlen(string);
-              if ((value_buf = (char *)calloc((size_t)++value_buf_size,
-                                              sizeof(char))) == NULL)
-                continue;
-              strlcpy(value_buf, string, value_buf_size);
-            } else {
-              DEBUG_printf((
-                  "_ppdConvertOptions: Unsupported attr: name=%s, value_tag=%d",
-                  ippGetName(attr), ippGetValueTag(attr)));
-              continue;
-            }
-          }
-          break;
-      }
-      if (value_buf)
-      {
-        ipp_attribute_t *option;
-        ipp_t           *collection = ippGetCollection(mapping, 0);
-
-
-        DEBUG_printf(
-            ("2_ppdConvertOptions: attr: name=%s, value_tag=%d, value=%s",
-             ippGetName(attr), ippGetValueTag(attr), value_buf));
+          ("2_ppdConvertOptions: attr: name=%s, value_tag=%d, value=%s",
+            ippGetName(attr), ippGetValueTag(attr), value_buf));
 
         /*
          * Add the IPP attribute value to each of the remapped options. It is
@@ -4544,9 +4719,16 @@
              option;
              option = ippNextAttribute(collection))
         {
-          char *option_value_buf = NULL;
-          const char *option_value_fmt = ippGetString(option, 0, NULL);
-          int option_value_size =
+          char       *option_value_buf = NULL;
+          const char *option_value_fmt;
+          int        option_value_size;
+
+          // Skip the meta attributes.
+          if (ippGetName(option)[0] == '_')
+            continue;
+
+          option_value_fmt = ippGetString(option, 0, NULL);
+          option_value_size =
               snprintf(NULL, 0, option_value_fmt, value_buf);
           if ((option_value_buf = (char *)calloc((size_t)++option_value_size,
                                                  sizeof(char))) == NULL)
@@ -4562,9 +4744,9 @@
         }
 
         free(value_buf);
-      }
     }
   }
+
   return num_options;
 }
 
diff --git a/option_mapping.md b/option_mapping.md
new file mode 100644
index 0000000..bce8e81
--- /dev/null
+++ b/option_mapping.md
@@ -0,0 +1,133 @@
+# IPP Attribute to PPD option mapping
+In order to support features that differ from PPD to PPD, the ppd-cache version
+of a printer now includes a series of mappings that are used to determine what
+options are passed to the filters. What _ppdConvertOptions expects to find is as
+follows:
+
+## option-mappings
+The (cupsd_printer_t*)p->ppd_attrs should contain an ipp collection called
+"option-mappings". It is expect that this collection will contain subcollections
+which indicate how IPP attributes found in a print job are to be converted into
+filter options. The name of these subcollections should be the name of the
+IPP attribute that should be translated:
+
+`
+ipp_t *option_mappings = ippFindAttribute(p->ppd_attrs,
+                                          "option-mappings",
+                                          IPP_TAG_BEGIN_COLLECTION);
+ipp_t *ipp_attr = ippNew();
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_TEXT, "setting", NULL, "True");
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_TEXT, "foo", NULL, "%s");
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_TEXT, "bar", NULL, "Custom.%s");
+ippAddCollection(option_mappings, IPP_TAG_ZERO, "ipp-attr", ipp_attr);
+`
+
+If a job comes in whose job->attrs contains an "ipp-attr" whose value is
+baz@Domain.com, then this mapping will result in the following three options
+being passed to the filter:
+
+`
+setting=True
+foo=baz@Domain.com
+bar=Custom.baz@Domain.com
+`
+
+The only substitutions allowed currently are "%s" for the transformed value of
+the attribute. Non-string values will be converted to strings.
+
+### transformations
+Sometimes IPP values cannot be converted directly into PPD values, so
+transformations can also be added to the collection. The name of a
+transformation must begin with "_", and the transformation should be in the same collection which contains the mappings for the specified ippAttribute:
+
+`
+ipp_t *option_mappings = ippFindAttribute(p->ppd_attrs,
+                                          "option-mappings",
+                                          IPP_TAG_BEGIN_COLLECTION);
+ipp_t *ipp_attr = ippNew();
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_TEXT, "bar", NULL, "Custom.%s");
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "_delimiters", NULL, "@");
+ippAddCollection(option_mappings, IPP_TAG_ZERO, "ipp-attr", ipp_attr);
+`
+
+Transformations are applied to the ipp value before it is added to the option
+strings. And they are applied in the order they are found in the collection.
+So in this case, if a job comes in whose job_attrs contains the
+"ipp-attr=baz@Domain.com", then the following options would be output:
+
+`
+bar=Custom.baz
+`
+
+The currently available transformations are:
+
+#### _delimiters
+Delimiters must always be a IPP_TAG_KEYWORD, and the string is the list of
+characters that terminate the value. So for example:
+
+`
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "_delimiters", NULL, "@");
+`
+
+Means that only characters before the first '@' in a ipp attribute value will
+be passed as an option value.
+
+#### _valid-chars
+Valid Chars must always be an IPP_TAG_KEYWORD, and the string is the list of
+characters that are allowed in as an option value. Any invalid character is
+replace with the character found in the _replacement-char transformation. So if
+the following transformations were in place:
+
+`
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "_valid-chars", NULL,
+             "abcdefghijklmnopqrstuvwxyz");
+ippAddString(ipp_attr, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "_replacement-char", NULL,
+             "-");
+`
+
+And the value of the specified ipp attribute was "ipp-attr=baz@Domain.com", the transformed value used for the options would be:
+
+`
+baz--omain-com
+`
+#### _constraint
+Constraint must always be an IPP_TAG_RANGE attribute, and it contains the
+minimum and maximum allowed characters for a filter option. If the value is
+shorter than the minimum, then the mapping is aborted and no options are passed
+to the filter for this transformation. If the value is longer than the maximum,
+then the string is truncated to that length. So if this transformation was in
+place:
+
+`
+ippAddRange(ipp_attr, IPP_TAG_ZERO, "_constraint", 2, 8);
+`
+
+And the value of the specified ipp attribute was "ipp-attr=baz@Domain.com", the transformed value used for the options would be:
+
+`
+baz@Doma
+`
+
+#### _mapping
+Mapping is always an ippCollection that consists of a series of IPP_TAG_KEYWORDs
+whose name is the IPP value and whose value is the PPD filter option value. If
+the ipp value can be found in the collection, then it is exchanged for the value
+of that keyword. If not, then the ipp attribute is discard and not converted
+into filter options. So if the following keywords existed in the mapping:
+
+`
+ipp_t *mapping = ippNew();
+ippAddString(mapping, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "baz", NULL, "bottle");
+ippAddString(mapping, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "bat", NULL, "ball");
+ippAddCollection(ipp_attr, IPP_TAG_ZERO, "_mapping", mapping);
+`
+
+And the value of the specified ipp attribute was "ipp-attr=baz", the transformed
+value used for the options would be:
+
+`
+bottle
+`
+
+
+
diff --git a/scheduler/printers.c b/scheduler/printers.c
index 75f99be..aec2bc3 100644
--- a/scheduler/printers.c
+++ b/scheduler/printers.c
@@ -5204,6 +5204,99 @@
     ippAddString(mapping, IPP_TAG_ZERO, IPP_TAG_TEXT, coption->keyword, NULL,
                  "Custom.%s");
   }
+  /*** Ricoh ***/
+  else if (((option = ppdFindOption(ppd, "JobType")) != NULL) &&
+           ((choice = ppdFindChoice(option, "LockedPrint")) != NULL))
+  {
+    ppd_coption_t *user_id_opt;
+    ppd_cparam_t  *user_id_param;
+
+    /*
+     * While all Ricoh printers use JobType=LockedPrint to indicate PIN
+     * printing, different printers use different custom options to pass the PIN
+     * to the device, so query the PPD to determine the name of the password
+     * option.
+     */
+    if ((((coption = ppdFindCustomOption(ppd, "Password")) != NULL) ||
+         ((coption = ppdFindCustomOption(ppd, "LockedPrintPassword")) != NULL) ||
+         ((coption = ppdFindCustomOption(ppd, "JobPassword")) != NULL)) &&
+        ((cparam = ppdFirstCustomParam(coption)) != NULL))
+    {
+      mapping = ippNew();
+
+      /*
+       * JobType = LockedPrint
+       */
+      ippAddString(mapping, IPP_TAG_ZERO, IPP_TAG_TEXT, option->keyword, NULL,
+                   choice->choice);
+
+      /*
+       * JobPassword = Custom.1234 or
+       * LockedPrintPassword = Custom.1234 or
+       * JobPassword = Custom.1234 or
+       */
+      ippAddString(mapping, IPP_TAG_ZERO, IPP_TAG_TEXT, coption->keyword, NULL,
+                   "Custom.%s");
+    }
+
+    /*
+      * The option name for User ID varies by PPD.
+      */
+    if ((((user_id_opt = ppdFindCustomOption(ppd, "UserId")) != NULL) ||
+          ((user_id_opt = ppdFindCustomOption(ppd, "UserID")) != NULL)) &&
+        ((user_id_param = ppdFirstCustomParam(user_id_opt)) != NULL))
+    {
+      /*
+      * Ricoh printers display the jobs by "User ID" rather than "User Name".
+      * User IDs must be between 1 & 8 characters, and can only contain the
+      * characters "a-z", "A-Z", "0-9", and "-./:_". If the Job ID is longer
+      * than 8 characters it is rejected. The IPP value to use is the
+      * job-originating-user-name, but since that is the full email address of
+      * the user, trim to just the username, then trim that to no more than 8
+      * characters, and replace any illegal characters with '-'.
+      */
+      ipp_t *userid_mapping = ippNew();
+
+      /*
+        * Remove domain from email address so User ID is just username.
+        */
+      ippAddString(userid_mapping, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "_delimiters",
+                   NULL, "@");
+
+      /*
+        * Ricoh specifies that the UserId must be a certain size.
+        */
+      ippAddRange(userid_mapping, IPP_TAG_ZERO, "_constraint",
+                  user_id_param->minimum.custom_string,
+                  user_id_param->maximum.custom_string);
+
+      /*
+        * Only the characters a-z,A-Z,0-9,-./:_ are valid for User ID.
+        */
+      ippAddString(userid_mapping, IPP_TAG_ZERO, IPP_TAG_KEYWORD,
+                   "_valid-chars", NULL,
+                    "abcdefghijklmnopqrstuvwxyz"
+                    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                    "0123456789"
+                    "-./:_");
+
+      /*
+        * Replace any non-valid characters with "-"
+        */
+      ippAddString(userid_mapping, IPP_TAG_ZERO, IPP_TAG_KEYWORD,
+                    "_replacement-char", NULL, "-");
+
+      /*
+        * UserId = Custom.%s or
+        * UserID = Custom.%s
+        */
+      ippAddString(userid_mapping, IPP_TAG_ZERO, IPP_TAG_TEXT,
+                   user_id_opt->keyword, NULL, "Custom.%s");
+
+      ippAddCollection(mappings, IPP_TAG_ZERO, "job-originating-user-name",
+                        userid_mapping);
+    }
+  }
 
   /* In order for a mapping to be establsihed the code above should have:
    * a) initialized mapping with the options to be sent to the filter