Support product-copy-files-by-pattern macro

Bug: 193566316
Test: rbcrun build/make/tests/run.rbc
Change-Id: Idf462d7f58e8d4a6e8b3a1506306f9eb67130dc8
diff --git a/core/product_config.rbc b/core/product_config.rbc
index 3714448..f7ce7aa 100644
--- a/core/product_config.rbc
+++ b/core/product_config.rbc
@@ -463,6 +463,28 @@
     print(message)
 
 
+def __mkparse_pattern(pattern):
+    """Parses Make's patsubst pattern."""
+    in_escape = False
+    res = []
+    acc = ""
+    for c in pattern.elems():
+        if in_escape:
+            in_escape = False
+            acc += c
+        elif c == '\\':
+            in_escape = True
+        elif c == '%' and not res:
+            res.append(acc)
+            acc = ''
+        else:
+            acc += c
+    if in_escape:
+        acc += '\\'
+    res.append(acc)
+    return res
+
+
 def __mkpatsubst_word(parsed_pattern,parsed_subst, word):
     (before, after) = parsed_pattern
     if not word.startswith(before):
@@ -480,16 +502,14 @@
     Tokenizes `s` (unless it is already a list), and then performs a simple
     wildcard substitution (in other words, `foo%bar` pattern is equivalent to
     the regular expression `^foo(.*)bar$, and the first `%` in replacement is
-    $1 in regex terms). Escaping % is not supported
+    $1 in regex terms).
     """
-    if pattern.find("\\") >= 0:
-        fail("'\\' in pattern is not allowed")
-    parsed_pattern = pattern.split("%", 1)
+    parsed_pattern = __mkparse_pattern(pattern)
     words = s if type(s) == "list" else _mkstrip(s).split(" ")
     if len(parsed_pattern) == 1:
         out_words = [ replacement if x == pattern else x for x in words]
     else:
-        parsed_replacement = replacement.split("%", 1)
+        parsed_replacement = __mkparse_pattern(replacement)
         out_words = [__mkpatsubst_word(parsed_pattern, parsed_replacement, x) for x in words]
     return out_words if type(s) == "list" else " ".join(out_words)
 
@@ -522,6 +542,22 @@
     return s.replace(old, new)
 
 
+def _product_copy_files_by_pattern(src, dest, s):
+    """Creates a copy list.
+
+    For each item in a given list, create <from>:<to> pair, where <from> and
+    <to> are the results of applying Make-style patsubst of <src> and <dest>
+    respectively. E.g. the result of calling this function with
+    ("foo/%", "bar/%", ["a", "b"])  will be
+    ["foo/a:bar/a", "foo/b:bar/b"].
+    """
+    parsed_src = __mkparse_pattern(src)
+    parsed_dest = __mkparse_pattern(dest)
+    parsed_percent = ["", ""]
+    words = s if type(s) == "list" else _mkstrip(s).split(" ")
+    return [ __mkpatsubst_word(parsed_percent, parsed_src, x) + ":" + __mkpatsubst_word(parsed_percent, parsed_dest, x) for x in words]
+
+
 def __get_options():
     """Returns struct containing runtime global settings."""
     settings = dict(
@@ -577,6 +613,7 @@
     mksubst = _mksubst,
     printvars = _printvars,
     product_configuration = _product_configuration,
+    product_copy_files_by_pattern = _product_copy_files_by_pattern,
     require_artifacts_in_path = _require_artifacts_in_path,
     require_artifacts_in_path_relaxed = _require_artifacts_in_path_relaxed,
     setdefault = _setdefault,
diff --git a/tests/run.rbc b/tests/run.rbc
index 15f6212..35ae19d 100644
--- a/tests/run.rbc
+++ b/tests/run.rbc
@@ -43,6 +43,9 @@
 assert_eq("azx b", rblf.mkpatsubst("az", "AZ", "azx  b"))
 assert_eq(["azx", "b"], rblf.mkpatsubst("az", "AZ", ["azx", "b"]))
 assert_eq("ABC", rblf.mkpatsubst("abc", "ABC", "abc"))
+assert_eq(["%/foo"], rblf.mkpatsubst("%", "\\%/%", ["foo"]))
+assert_eq(["foo/%"], rblf.mkpatsubst("%", "%/%", ["foo"]))
+assert_eq(["from/a:to/a", "from/b:to/b"], rblf.product_copy_files_by_pattern("from/%", "to/%", "a b"))
 
 globals, config = rblf.product_configuration("test/device", init)
 assert_eq(