@Contract("null, _, _ -> null")
  private static String toCanonicalPath(
      @Nullable String path, char separatorChar, boolean removeLastSlash) {
    if (path == null || path.isEmpty()) {
      return path;
    } else if (".".equals(path)) {
      return "";
    }

    path = path.replace(separatorChar, '/');
    if (path.indexOf('/') == -1) {
      return path;
    }

    int start = pathRootEnd(path) + 1, dots = 0;
    boolean separator = true;

    StringBuilder result = new StringBuilder(path.length());
    result.append(path, 0, start);

    for (int i = start; i < path.length(); ++i) {
      char c = path.charAt(i);
      if (c == '/') {
        if (!separator) {
          processDots(result, dots, start);
          dots = 0;
        }
        separator = true;
      } else if (c == '.') {
        if (separator || dots > 0) {
          ++dots;
        } else {
          result.append('.');
        }
        separator = false;
      } else {
        if (dots > 0) {
          StringUtil.repeatSymbol(result, '.', dots);
          dots = 0;
        }
        result.append(c);
        separator = false;
      }
    }

    if (dots > 0) {
      processDots(result, dots, start);
    }

    int lastChar = result.length() - 1;
    if (removeLastSlash && lastChar >= 0 && result.charAt(lastChar) == '/' && lastChar > start) {
      result.deleteCharAt(lastChar);
    }

    return result.toString();
  }