docs: private void scanDirRecursively(final File dir, final LogNode log) {
          if (skipClasspathElement) {
              return;
          }
          // See if this canonical path has been scanned before, so that recursive scanning doesn't get stuck in an
          // infinite loop due to symlinks
          String canonicalPath;
          try {
              canonicalPath = dir.getCanonicalPath();
              if (!scannedCanonicalPaths.add(canonicalPath)) {
                  if (log != null) {
                      log.log("Reached symlink cycle, stopping recursion: " + dir);
                  }
                  return;
              }
          } catch (final IOException | SecurityException e) {
              if (log != null) {
                  log.log("Could not canonicalize path: " + dir, e);
              }
              return;
          }
  
          final String dirPath = dir.getPath();
          final String dirRelativePath = ignorePrefixLen > dirPath.length() ? "/" //
                  : dirPath.substring(ignorePrefixLen).replace(File.separatorChar, '/') + "/";
  
          if (nestedClasspathRootPrefixes != null && nestedClasspathRootPrefixes.contains(dirRelativePath)) {
              if (log != null) {
                  log.log("Reached nested classpath root, stopping recursion to avoid duplicate scanning: "
                          + dirRelativePath);
              }
              return;
          }
  
          // Whitelist/blacklist classpath elements based on dir resource paths
          checkResourcePathWhiteBlackList(dirRelativePath, log);
          if (skipClasspathElement) {
              return;
          }
  
          final ScanSpecPathMatch parentMatchStatus = scanSpec.dirWhitelistMatchStatus(dirRelativePath);
          if (parentMatchStatus == ScanSpecPathMatch.HAS_BLACKLISTED_PATH_PREFIX) {
              // Reached a non-whitelisted or blacklisted path -- stop the recursive scan
              if (log != null) {
                  log.log("Reached blacklisted directory, stopping recursive scan: " + dirRelativePath);
              }
              return;
          }
          if (parentMatchStatus == ScanSpecPathMatch.NOT_WITHIN_WHITELISTED_PATH) {
              // Reached a non-whitelisted and non-blacklisted path -- stop the recursive scan
              return;
          }
  
          final File[] filesInDir = dir.listFiles();
          if (filesInDir == null) {
              if (log != null) {
                  log.log("Invalid directory " + dir);
              }
              return;
          }
          Arrays.sort(filesInDir);
          final LogNode subLog = log == null ? null
                  // Log dirs after files (addWhitelistedResources() precedes log entry with "0:")
                  : log.log("1:" + canonicalPath, "Scanning directory: " + dir
                          + (dir.getPath().equals(canonicalPath) ? "" : " ; canonical path: " + canonicalPath));
  
          // Only scan files in directory if directory is not only an ancestor of a whitelisted path
          if (parentMatchStatus != ScanSpecPathMatch.ANCESTOR_OF_WHITELISTED_PATH) {
              // Do preorder traversal (files in dir, then subdirs), to reduce filesystem cache misses
              for (final File fileInDir : filesInDir) {
                  // Process files in dir before recursing
                  if (fileInDir.isFile()) {
                      final String fileInDirRelativePath = dirRelativePath.isEmpty() || "/".equals(dirRelativePath)
                              ? fileInDir.getName()
                              : dirRelativePath + fileInDir.getName();
  
                      // Whitelist/blacklist classpath elements based on file resource paths
                      checkResourcePathWhiteBlackList(fileInDirRelativePath, subLog);
                      if (skipClasspathElement) {
                          return;
                      }
  
                      // If relative path is whitelisted
                      if (parentMatchStatus == ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX
                              || parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_PATH
                              || (parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_CLASS_PACKAGE
                                      && scanSpec.classfileIsSpecificallyWhitelisted(fileInDirRelativePath))) {
                          // Resource is whitelisted
                          final Resource resource = newResource(fileInDirRelativePath, fileInDir);
                          addWhitelistedResource(resource, parentMatchStatus, subLog);
  
                          // Save last modified time  
                          fileToLastModified.put(fileInDir, fileInDir.lastModified());
                      } else {
                          if (subLog != null) {
                              subLog.log("Skipping non-whitelisted file: " + fileInDirRelativePath);
                          }
                      }
                  }
              }
          } else if (scanSpec.enableClassInfo && dirRelativePath.equals("/")) {
              // Always check for module descriptor in package root, even if package root isn't in whitelist
              for (final File fileInDir : filesInDir) {
                  if (fileInDir.getName().equals("module-info.class") && fileInDir.isFile()) {
                      final Resource resource = newResource("module-info.class", fileInDir);
                      addWhitelistedResource(resource, parentMatchStatus, subLog);
                      fileToLastModified.put(fileInDir, fileInDir.lastModified());
                  }
              }
          }
          // Recurse into subdirectories
          for (final File fileInDir : filesInDir) {
              if (fileInDir.isDirectory()) {
                  scanDirRecursively(fileInDir, subLog);
                  // If a blacklisted classpath element resource path was found, it will set skipClasspathElement
                  if (skipClasspathElement) {
                      if (subLog != null) {
                          subLog.addElapsedTime();
                      }
                      return;
                  }
              }
          }
  
          if (subLog != null) {
              subLog.addElapsedTime();
          }
  
          // Save the last modified time of the directory
          fileToLastModified.put(dir, dir.lastModified());
      }