commit 740c6915ff9efa714c50f378276f70357b44cbc7
Author: Dmitry Goncharov <dgoncharov@users.sf.net>
Date:   Sun Mar 20 21:12:15 2022 -0400

    [SV 62206] Fix % substitution in pattern rules.
    
    Support multiple patterns in each prerequisite, during second expansion of
    pattern rule prerequisites.
    E.g. given makefile
    
    .SECONDEXPANSION:
    all: hello.x
    %.x: $$(wordlist 1, 99, %.1 %.2) $$(wordlist 1, 99, %.3 %.4); $(info $@ from $^)
    
    this commit implements substitution of '%' with 'hello' in '%.2' and '%.4'.
    
    * src/rule.c (snap_implicit_rules): During max_pattern_dep_length calculation
    count each % as 5 bytes when second expansion is enabled to preallocate
    enough memory for $(*F).
    * src/implicit.c (pattern_search): When second expansion is enabled,
    substitute the first % of each prerequisite word with *$ or $(*F).
    * tests/scripts/features/patternrules_stem: Test that make correctly computes
    stem and substitutes % for computed stem in pattern rules with and without
    second expansion and with and without last slash.

diff --git a/src/implicit.c b/src/implicit.c
index b7f5d9ce..2dc9df30 100644
--- a/src/implicit.c
+++ b/src/implicit.c
@@ -230,7 +230,9 @@ pattern_search (struct file *file, int archive,
 
   /* Names of possible dependencies are constructed in this buffer.
      We may replace % by $(*F) for second expansion, increasing the length.  */
-  char *depname = alloca (namelen + max_pattern_dep_length + 4);
+  size_t deplen = namelen + max_pattern_dep_length + 4;
+  char *depname = alloca (deplen);
+  char *dend = depname + deplen;
 
   /* The start and length of the stem of FILENAME for the current rule.  */
   const char *stem = 0;
@@ -601,6 +603,7 @@ pattern_search (struct file *file, int archive,
                 {
                   int add_dir = 0;
                   size_t len;
+                  const char *end;
                   struct dep **dptr;
                   int is_explicit;
                   const char *cp;
@@ -609,12 +612,14 @@ pattern_search (struct file *file, int archive,
                   nptr = get_next_word (nptr, &len);
                   if (nptr == 0)
                     continue;
+                  end = nptr + len;
+
 
                   /* See if this is a transition to order-only prereqs.  */
                   if (! order_only && len == 1 && nptr[0] == '|')
                     {
                       order_only = 1;
-                      nptr += len;
+                      nptr = end;
                       continue;
                     }
 
@@ -629,7 +634,7 @@ pattern_search (struct file *file, int archive,
                      (since $* and $(*F) are simple variables) there won't be
                      additional re-expansion of the stem.  */
 
-                  cp = lindex (nptr, nptr + len, '%');
+                  cp = lindex (nptr, end, '%');
                   if (cp == 0)
                     {
                       memcpy (depname, nptr, len);
@@ -638,28 +643,63 @@ pattern_search (struct file *file, int archive,
                     }
                   else
                     {
-                      size_t i = cp - nptr;
+                      /* Go through all % between NPTR and END.
+                       * Copy contents of [NPTR, END) to depname, with
+                       * the first % after NPTR and then each first % after
+                       * white space replaced with *$ or $(*F).
+                       * depname has enough room to substitute each % with
+                       * $(*F).  */
                       char *o = depname;
-                      memcpy (o, nptr, i);
-                      o += i;
-                      if (check_lastslash)
-                        {
-                          add_dir = 1;
-                          memcpy (o, "$(*F)", 5);
-                          o += 5;
-                        }
-                      else
+
+                      is_explicit = 0;
+                      for (;;)
                         {
-                          memcpy (o, "$*", 2);
-                          o += 2;
+                          size_t i = cp - nptr;
+                          assert (o + i < dend);
+                          memcpy (o, nptr, i);
+                          o += i;
+                          if (check_lastslash)
+                            {
+                              add_dir = 1;
+                              assert (o + 5 < dend);
+                              memcpy (o, "$(*F)", 5);
+                              o += 5;
+                            }
+                          else
+                            {
+                              assert (o + 2 < dend);
+                              memcpy (o, "$*", 2);
+                              o += 2;
+                            }
+                          assert (o < dend);
+                          assert (cp + 1 <= end);
+                          nptr = cp + 1;
+                          if (nptr >= end)
+                            break;
+
+                          /* No need to worry about order-only, or nested
+                           * function calls, because NPTR went though
+                           * get_next_word.  */
+                          i = strcspn (nptr, " \t");
+                          if (nptr + i >= end)
+                            break;
+                          assert (o + i < dend);
+                          assert (nptr + i < end);
+                          memcpy (o, nptr, i);
+                          o += i;
+                          nptr += i;
+
+                          cp = lindex (nptr, end, '%');
+                          if (cp == 0)
+                            break;
                         }
-                      memcpy (o, cp + 1, len - i - 1);
-                      o[len - i - 1] = '\0';
-                      is_explicit = 0;
+                        len = end - nptr;
+                        memcpy (o, nptr, len);
+                        o[len] = '\0';
                     }
 
                   /* Set up for the next word.  */
-                  nptr += len;
+                  nptr = end;
 
                   /* Initialize and set file variables if we haven't already
                      done so. */
diff --git a/src/rule.c b/src/rule.c
index 7b12413e..aaff0255 100644
--- a/src/rule.c
+++ b/src/rule.c
@@ -129,7 +129,13 @@ snap_implicit_rules (void)
 
   for (dep = prereqs; dep; dep = dep->next)
     {
-      size_t l = strlen (dep_name (dep));
+      /* When pattern_search allocates a buffer of size max_pattern_dep_length,
+       * having 5 bytes per each % lets substitute each % with $(*F) and avoids
+       * realloc.  */
+      size_t l = 0;
+      const char *d = dep_name (dep);
+      for (; *d; ++d)
+        l += dep->need_2nd_expansion && *d == '%' ? 5 : 1;
       if (l > max_pattern_dep_length)
         max_pattern_dep_length = l;
       ++pre_deps;
