[exim-cvs] Expansions: extract methods for JSON objects and …

Kezdőlap
Üzenet törlése
Válasz az üzenetre
Szerző: Exim Git Commits Mailing List
Dátum:  
Címzett: exim-cvs
Tárgy: [exim-cvs] Expansions: extract methods for JSON objects and arrays. Bug 2282
Gitweb: https://git.exim.org/exim.git/commitdiff/8fdf20fd84ec88d8f8a250f56d2b4d29ba946392
Commit:     8fdf20fd84ec88d8f8a250f56d2b4d29ba946392
Parent:     c092711614b9b12df56869a95bc9609f913e7be3
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Sun Sep 30 01:08:51 2018 +0100
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Sun Sep 30 01:08:51 2018 +0100


    Expansions: extract methods for JSON objects and arrays.  Bug 2282
---
 doc/doc-docbook/spec.xfpt    |  34 +++++++
 doc/doc-txt/NewStuff         |   2 +
 src/src/expand.c             | 208 +++++++++++++++++++++++++++++++++++++------
 test/scripts/0000-Basic/0002 |  21 +++++
 test/stdout/0002             |   9 ++
 5 files changed, 248 insertions(+), 26 deletions(-)


diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index 8fde639..1dfa552 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -9374,6 +9374,27 @@ ${extract{Z}{A=... B=...}{$value} fail }
This forces an expansion failure (see section &<<SECTforexpfai>>&);
{<&'string2'&>} must be present for &"fail"& to be recognized.

+.new
+.vitem "&*${extract json{*&<&'key'&>&*}{*&<&'string1'&>&*}{*&<&'string2'&>&*}&&&
+       {*&<&'string3'&>&*}}*&"
+.cindex "expansion" "extracting from JSON object"
+.cindex JSON expansions
+The key and <&'string1'&> are first expanded separately. Leading and trailing
+white space is removed from the key (but not from any of the strings). The key
+must not be empty and must not consist entirely of digits.
+The expanded <&'string1'&> must be of the form:
+.display
+{ <&'"key1"'&> : <&'value1'&> ,  <&'"key2"'&> , <&'value2'&> ... }
+.endd
+.vindex "&$value$&"
+The braces, commas and colons, and the quoting of the member name are required;
+the spaces are optional.
+Matching of the key against the member names is done case-sensitively.
+. XXX should be a UTF-8 compare
+
+The results of matching are handled as above.
+.wen
+


 .vitem "&*${extract{*&<&'number'&>&*}{*&<&'separators'&>&*}&&&
         {*&<&'string1'&>&*}{*&<&'string2'&>&*}{*&<&'string3'&>&*}}*&"
@@ -9406,6 +9427,19 @@ yields &"99"&. Two successive separators mean that the field between them is
 empty (for example, the fifth field above).



+.new
+.vitem "&*${extract json{*&<&'number'&>&*}}&&&
+        {*&<&'string1'&>&*}{*&<&'string2'&>&*}{*&<&'string3'&>&*}}*&"
+.cindex "expansion" "extracting from JSON array"
+.cindex JSON expansions
+The <&'number'&> argument must consist entirely of decimal digits,
+apart from leading and trailing white space, which is ignored.
+
+Field selection and result handling is as above;
+there is no choice of field separator.
+.wen
+
+
 .vitem &*${filter{*&<&'string'&>&*}{*&<&'condition'&>&*}}*&
 .cindex "list" "selecting by condition"
 .cindex "expansion" "selecting from list by condition"
diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index 8d2bf22..3f25720 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -25,6 +25,8 @@ Version 4.92


6. Builtin macros for supported log_selector and openssl_options values.

+ 7. JSON variants of the ${extract } expansion item.
+
Version 4.91
--------------

diff --git a/src/src/expand.c b/src/src/expand.c
index cd753ef..43f572e 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -1133,20 +1133,20 @@ Returns:    NULL if the subfield was not found, or
 */


static uschar *
-expand_getkeyed(uschar *key, const uschar *s)
+expand_getkeyed(uschar * key, const uschar * s)
{
int length = Ustrlen(key);
while (isspace(*s)) s++;

/* Loop to search for the key */

-while (*s != 0)
+while (*s)
{
int dkeylength;
- uschar *data;
- const uschar *dkey = s;
+ uschar * data;
+ const uschar * dkey = s;

- while (*s != 0 && *s != '=' && !isspace(*s)) s++;
+ while (*s && *s != '=' && !isspace(*s)) s++;
dkeylength = s - dkey;
while (isspace(*s)) s++;
if (*s == '=') while (isspace((*(++s))));
@@ -1257,17 +1257,17 @@ return fieldtext;
static uschar *
expand_getlistele(int field, const uschar * list)
{
-const uschar * tlist= list;
-int sep= 0;
+const uschar * tlist = list;
+int sep = 0;
uschar dummy;

-if(field<0)
+if (field < 0)
{
- for(field++; string_nextinlist(&tlist, &sep, &dummy, 1); ) field++;
- sep= 0;
+ for (field++; string_nextinlist(&tlist, &sep, &dummy, 1); ) field++;
+ sep = 0;
}
-if(field==0) return NULL;
-while(--field>0 && (string_nextinlist(&list, &sep, &dummy, 1))) ;
+if (field == 0) return NULL;
+while (--field > 0 && (string_nextinlist(&list, &sep, &dummy, 1))) ;
return string_nextinlist(&list, &sep, NULL, 0);
}

@@ -3849,6 +3849,79 @@ return x;



+/* Return pointer to dewrapped string, with enclosing specified chars removed.
+The given string is modified on return.  Leading whitespace is skipped while
+looking for the opening wrap character, then the rest is scanned for the trailing
+(non-escaped) wrap character.  A backslash in the string will act as an escape.
+
+A nul is written over the trailing wrap, and a pointer to the char after the
+leading wrap is returned.
+
+Arguments:
+  s    String for de-wrapping
+  wrap  Two-char string, the first being the opener, second the closer wrapping
+        character
+Return:
+  Pointer to de-wrapped string, or NULL on error (with expand_string_message set).
+*/
+
+static uschar *
+dewrap(uschar * s, const uschar * wrap)
+{
+uschar * p = s;
+
+while (isspace(*p)) p++;
+
+if (*p == *wrap)
+  {
+  s = ++p;
+  wrap++;
+  while (*p)
+    {
+    if (*p == '\\') p++;
+    else if (*p == *wrap)
+      {
+      *p = '\0';
+      return s;
+      }
+    p++;
+    }
+  }
+expand_string_message = string_sprintf("missing '%c'", *wrap);
+return NULL;
+}
+
+
+/* Pull off the leading array or object element, returning
+a copy in an allocated string.  Update the list pointer.
+
+The element may itself be an abject or array.
+*/
+
+uschar *
+json_nextinlist(const uschar ** list)
+{
+unsigned array_depth = 0, object_depth = 0;
+const uschar * s = *list, * item;
+
+while (isspace(*s)) s++;
+
+for (item = s;
+     *s && (*s != ',' || array_depth != 0 || object_depth != 0);
+     s++)
+  switch (*s)
+    {
+    case '[': array_depth++; break;
+    case ']': array_depth--; break;
+    case '{': object_depth++; break;
+    case '}': object_depth--; break;
+    }
+*list = *s ? s+1 : s;
+return string_copyn(item, s - item);
+}
+
+
+
 /*************************************************
 *                 Expand string                  *
 *************************************************/
@@ -5554,6 +5627,16 @@ while (*s != 0)
       uschar *sub[3];
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
+      enum {extract_basic, extract_json} fmt = extract_basic;
+
+      while (isspace(*s)) s++;
+
+      /* Check for a format-variant specifier */
+
+      if (*s != '{')                    /*}*/
+    {
+    if (Ustrncmp(s, "json", 4) == 0) {fmt = extract_json; s += 4;}
+    }


       /* While skipping we cannot rely on the data for expansions being
       available (eg. $item) hence cannot decide on numeric vs. keyed.
@@ -5561,11 +5644,10 @@ while (*s != 0)


       if (skipping)
     {
-        while (isspace(*s)) s++;
-        for (j = 5; j > 0 && *s == '{'; j--)
+        for (j = 5; j > 0 && *s == '{'; j--)            /*'}'*/
       {
           if (!expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok))
-        goto EXPAND_FAILED;                    /*{*/
+        goto EXPAND_FAILED;                    /*'{'*/
           if (*s++ != '}')
         {
         expand_string_message = US"missing '{' for arg of extract";
@@ -5573,13 +5655,13 @@ while (*s != 0)
         }
       while (isspace(*s)) s++;
       }
-    if (  Ustrncmp(s, "fail", 4) == 0
+    if (  Ustrncmp(s, "fail", 4) == 0            /*'{'*/
        && (s[4] == '}' || s[4] == ' ' || s[4] == '\t' || !s[4])
        )
       {
       s += 4;
       while (isspace(*s)) s++;
-      }
+      }                            /*'{'*/
     if (*s != '}')
       {
       expand_string_message = US"missing '}' closing extract";
@@ -5589,11 +5671,11 @@ while (*s != 0)


       else for (i = 0, j = 2; i < j; i++) /* Read the proper number of arguments */
         {
-        while (isspace(*s)) s++;
-        if (*s == '{')                         /*}*/
+    while (isspace(*s)) s++;
+        if (*s == '{')                         /*'}'*/
           {
           sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
-          if (sub[i] == NULL) goto EXPAND_FAILED;        /*{*/
+          if (sub[i] == NULL) goto EXPAND_FAILED;        /*'{'*/
           if (*s++ != '}')
         {
         expand_string_message = string_sprintf(
@@ -5604,7 +5686,7 @@ while (*s != 0)
           /* After removal of leading and trailing white space, the first
           argument must not be empty; if it consists entirely of digits
           (optionally preceded by a minus sign), this is a numerical
-          extraction, and we expect 3 arguments. */
+          extraction, and we expect 3 arguments (normal) or 2 (json). */


           if (i == 0)
             {
@@ -5635,7 +5717,7 @@ while (*s != 0)
         if (*p == 0)
           {
           field_number *= x;
-          j = 3;               /* Need 3 args */
+          if (fmt != extract_json) j = 3;               /* Need 3 args */
           field_number_set = TRUE;
           }
             }
@@ -5651,9 +5733,83 @@ while (*s != 0)
       /* Extract either the numbered or the keyed substring into $value. If
       skipping, just pretend the extraction failed. */


-      lookup_value = skipping? NULL : field_number_set?
-        expand_gettokened(field_number, sub[1], sub[2]) :
-        expand_getkeyed(sub[0], sub[1]);
+      if (skipping)
+    lookup_value = NULL;
+      else switch (fmt)
+    {
+    case extract_basic:
+      lookup_value = field_number_set
+        ? expand_gettokened(field_number, sub[1], sub[2])
+        : expand_getkeyed(sub[0], sub[1]);
+      break;
+
+    case extract_json:
+      {
+      uschar * s, * item;
+      const uschar * list;
+
+      /* Array: Bracket-enclosed and comma-separated.
+      Object: Brace-enclosed, comma-sep list of name:value pairs */
+
+      if (!(s = dewrap(sub[1], field_number_set ? US"[]" : US"{}")))
+        {
+        expand_string_message =
+          string_sprintf("%s wrapping %s for extract json",
+        expand_string_message,
+        field_number_set ? "array" : "object");
+        goto EXPAND_FAILED_CURLY;
+        }
+
+      list = s;
+      if (field_number_set)
+        {
+        if (field_number <= 0)
+          {
+          expand_string_message = US"first argument of \"extract\" must "
+        "be greater than zero";
+          goto EXPAND_FAILED;
+          }
+        while (field_number > 0 && (item = json_nextinlist(&list)))
+          field_number--;
+        s = item;
+        lookup_value = s;
+        while (*s) s++;
+        while (--s >= lookup_value && isspace(*s)) *s = '\0';
+        }
+      else
+        {
+        lookup_value = NULL;
+        while ((item = json_nextinlist(&list)))
+          {
+          /* Item is:  string name-sep value.  string is quoted.
+          Dequote the string and compare with the search key. */
+
+          if (!(item = dewrap(item, US"\"\"")))
+        {
+        expand_string_message =
+          string_sprintf("%s wrapping string key for extract json",
+            expand_string_message);
+        goto EXPAND_FAILED_CURLY;
+        }
+          if (Ustrcmp(item, sub[0]) == 0)    /*XXX should be a UTF8-compare */
+        {
+        s = item + strlen(item) + 1;
+        while (isspace(*s)) s++;
+        if (*s != ':')
+          {
+          expand_string_message = string_sprintf(
+            "missing object value-separator for extract json");
+          goto EXPAND_FAILED_CURLY;
+          }
+        s++;
+        while (isspace(*s)) s++;
+        lookup_value = s;
+        break;
+        }
+          }
+        }
+      }
+    }


       /* If no string follows, $value gets substituted; otherwise there can
       be yes/no strings, as for lookup or if. */
@@ -5753,7 +5909,7 @@ while (*s != 0)
       /* Extract the numbered element into $value. If
       skipping, just pretend the extraction failed. */


-      lookup_value = skipping? NULL : expand_getlistele(field_number, sub[1]);
+      lookup_value = skipping ? NULL : expand_getlistele(field_number, sub[1]);


       /* If no string follows, $value gets substituted; otherwise there can
       be yes/no strings, as for lookup or if. */
diff --git a/test/scripts/0000-Basic/0002 b/test/scripts/0000-Basic/0002
index 27bf708..65ce40b 100644
--- a/test/scripts/0000-Basic/0002
+++ b/test/scripts/0000-Basic/0002
@@ -874,6 +874,27 @@ ${if ={1}{1} {true}{${if ={1}{1} {true}{${if ={1}{1}{true}fail}}}}}
 ${env {USER}}
 ${env {NO_SUCH_VARIABLE} {oops, success} {correct}}


+# JSON
+
+${extract json {Url} \
+  {   \{ \"Url\":    \"http://www.example.com/image/481989943\",\
+     \"Height\": 125,\
+     \"Width\":  100\
+      \} \
+  } \
+ }
+${extract json {Width} \
+  {   \{ \"Url\":    \"http://www.example.com/image/481989943\",\
+     \"Height\": 125,\
+     \"Width\":  100\
+      \} \
+  } \
+ }
+${extract json {2} {[116, 943, 234, 38793]} }
+${extract json {2} {${extract json{IDs} {\{"other":"foo", "IDs": [116, 943, 234]\} }}} }
+
+${extract json {seconds} { \{"hours":0, "mins":0, "seconds":59\} }}
+${extract json {seconds} {${extract json {2} { ["irrelevant", \{"hours":0, "mins":0, "seconds":59\}] }}}}
 ****
 # Test "escape" with print_topbitchars
 exim -be -DPTBC=print_topbitchars
diff --git a/test/stdout/0002 b/test/stdout/0002
index 20f4183..a8d1361 100644
--- a/test/stdout/0002
+++ b/test/stdout/0002
@@ -829,6 +829,15 @@ xyz

> CALLER
> correct
>

+> # JSON
+>
+> "http://www.example.com/image/481989943"
+> 100
+> 943
+> 943
+>
+> 59
+> 59
>
> escape: B7?F2?
>