BasicAuthentication.getRootPath exhibits quadratic worst-case time complexity (Θ(n²)) for certain inputs

XMLWordPrintable

      A DESCRIPTION OF THE PROBLEM :
      getRootPath(String npath, String opath) iterates over slash boundaries in opath and repeatedly calls:
      ```
      opath.regionMatches(0, npath, 0, toindex + 1)

      ```
      Each iteration compares the prefix starting from index 0 up to a growing toindex. When npath and opath share a long prefix and opath contains many '/', this leads to repeated re-scanning of the same characters.
      Hence, the method has a Θ(n²) worst case. This is avoidable: the common root path can be computed in Θ(n) using a single left-to-right scan while tracking the last matching '/'.Even though URLs are usually subject to length limits, from a stability perspective this is still an issue that should be fixed.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Save the test program shown in Test case. It copies the JDK method logic and adds a comparison-counting variant to avoid timing noise.Compile and run.
      Observe that comparisons grows ~quadratically with len(n), and comp/n^2 approaches a constant.

      ---------- BEGIN SOURCE ----------
      ```
      package getRootPath;

      import java.net.URI;
      import java.net.URISyntaxException;
      import java.util.Locale;

      public class RootPathQuadraticProof {

          // Original function (kept for optional timing comparison)
          static String getRootPath(String npath, String opath) {
              int index = 0;
              int toindex;

              try {
                  npath = new URI(npath).normalize().getPath();
                  opath = new URI(opath).normalize().getPath();
              } catch (URISyntaxException e) {
                  // ignore
              }

              while (index < opath.length()) {
                  toindex = opath.indexOf('/', index + 1);
                  if (toindex != -1 && opath.regionMatches(0, npath, 0, toindex + 1)) {
                      index = toindex;
                  } else {
                      return opath.substring(0, index + 1);
                  }
              }
              return npath;
          }

          static final class Counter {
              long charComparisons = 0;
          }

          // Character-by-character region match that counts comparisons
          static boolean regionMatchesCount(String a, int aOff, String b, int bOff, int len, Counter c) {
              if (aOff < 0 || bOff < 0) return false;
              if (aOff + len > a.length() || bOff + len > b.length()) return false;

              for (int i = 0; i < len; i++) {
                  c.charComparisons++;
                  if (a.charAt(aOff + i) != b.charAt(bOff + i)) return false;
              }
              return true;
          }

          // Same logic as getRootPath, but replaces regionMatches with regionMatchesCount
          static String getRootPathCount(String npath, String opath, Counter c) {
              int index = 0;
              int toindex;

              try {
                  npath = new URI(npath).normalize().getPath();
                  opath = new URI(opath).normalize().getPath();
              } catch (URISyntaxException e) {
                  // ignore
              }

              while (index < opath.length()) {
                  toindex = opath.indexOf('/', index + 1);
                  if (toindex != -1 && regionMatchesCount(opath, 0, npath, 0, toindex + 1, c)) {
                      index = toindex;
                  } else {
                      return opath.substring(0, index + 1);
                  }
              }
              return npath;
          }

          // Worst-case style input: many short segments with lots of '/' and identical prefixes
          // Example: "/a/a/a/.../a/"
          static String makeWorstPath(int segments) {
              StringBuilder sb = new StringBuilder(segments * 3);
              for (int i = 0; i < segments; i++) sb.append("/a/");
              return sb.toString();
          }

          // Timing helper (less reliable than counting comparisons, but useful as a secondary signal)
          static long timeNanosPerCall(int iters, String npath, String opath) {
              long t0 = System.nanoTime();
              int sink = 0;
              for (int i = 0; i < iters; i++) {
                  sink ^= getRootPath(npath, opath).length();
              }
              long t1 = System.nanoTime();
              if (sink == 42) System.out.println("sink"); // prevent dead-code elimination
              return (t1 - t0) / iters;
          }

          public static void main(String[] args) {
              Locale.setDefault(Locale.ROOT);

              int[] sizes = {200, 400, 800, 1600, 3200, 6400,12800,25600};

              System.out.println("== Quadratic evidence via counted character comparisons ==");
              String h1 = "%10s %10s %18s %14s %14s%n";
              String r1 = "%10d %10d %18d %14.3f %14.9f%n";
              System.out.printf(h1, "segments", "len(n)", "comparisons", "comp/n", "comp/n^2");

              for (int seg : sizes) {
                  String opath = makeWorstPath(seg);
                  String npath = new String(opath);

                  Counter c = new Counter();
                  getRootPathCount(npath, opath, c);

                  double n = opath.length();
                  double comp = c.charComparisons;
                  System.out.printf(r1, seg, opath.length(), c.charComparisons, comp / n, comp / (n * n));
              }

              System.out.println();
              System.out.println("== Optional timing (subject to JIT/GC/CPU variability) ==");
              String h2 = "%10s %10s %14s %14s %14s%n";
              String r2 = "%10d %10d %14d %14.3f %14.9f%n";
              System.out.printf(h2, "segments", "len(n)", "avg_ns", "ns/n", "ns/n^2");

              // Warm up for JIT
              for (int i = 0; i < 4000; i++) {
                  String p = makeWorstPath(200);
                  getRootPath(new String(p), p);
              }

              int iters = 50;
              for (int seg : sizes) {
                  String opath = makeWorstPath(seg);
                  String npath = new String(opath);

                  long avgNs = timeNanosPerCall(iters, npath, opath);
                  double n = opath.length();
                  System.out.printf(r2, seg, opath.length(), avgNs, avgNs / n, avgNs / (n * n));
              }
          }
      }

      ```
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Replace the repeated prefix regionMatches loop with a single linear scan tracking the last matching slash

      FREQUENCY :
      ALWAYS

            Assignee:
            Unassigned
            Reporter:
            Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: