docs: private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, final LogNode log)
              throws IOException, InterruptedException {
          // Scan for End Of Central Directory (EOCD) signature
          long eocdPos = -1;
          for (long i = len - 22; i >= 0; --i) {
              if (zipFileSliceReader.getInt(i) == 0x06054b50) {
                  eocdPos = i;
                  break;
              }
          }
          if (eocdPos < 0) {
              throw new IOException("Jarfile central directory signature not found: " + getPath());
          }
          long numEnt = zipFileSliceReader.getShort(eocdPos + 8);
          if (zipFileSliceReader.getShort(eocdPos + 4) > 0 || zipFileSliceReader.getShort(eocdPos + 6) > 0
                  || numEnt != zipFileSliceReader.getShort(eocdPos + 10)) {
              throw new IOException("Multi-disk jarfiles not supported: " + getPath());
          }
          long cenSize = zipFileSliceReader.getInt(eocdPos + 12);
          if (cenSize > eocdPos) {
              throw new IOException(
                      "Central directory size out of range: " + cenSize + " vs. " + eocdPos + ": " + getPath());
          }
          long cenOff = zipFileSliceReader.getInt(eocdPos + 16);
          long cenPos = eocdPos - cenSize;
  
          // Check for Zip64 End Of Central Directory Locator record
          final long zip64cdLocIdx = eocdPos - 20;
          if (zip64cdLocIdx >= 0 && zipFileSliceReader.getInt(zip64cdLocIdx) == 0x07064b50) {
              if (zipFileSliceReader.getInt(zip64cdLocIdx + 4) > 0
                      || zipFileSliceReader.getInt(zip64cdLocIdx + 16) > 1) {
                  throw new IOException("Multi-disk jarfiles not supported: " + getPath());
              }
              final long eocdPos64 = zipFileSliceReader.getLong(zip64cdLocIdx + 8);
              if (zipFileSliceReader.getInt(eocdPos64) != 0x06064b50) {
                  throw new IOException("Zip64 central directory at location " + eocdPos64
                          + " does not have Zip64 central directory header: " + getPath());
              }
              final long numEnt64 = zipFileSliceReader.getLong(eocdPos64 + 24);
              if (zipFileSliceReader.getInt(eocdPos64 + 16) > 0 || zipFileSliceReader.getInt(eocdPos64 + 20) > 0
                      || numEnt64 != zipFileSliceReader.getLong(eocdPos64 + 32)) {
                  throw new IOException("Multi-disk jarfiles not supported: " + getPath());
              }
              if (numEnt != numEnt64 && numEnt != 0xffff) {
                  // Entry size mismatch -- trigger manual counting of entries
                  numEnt = -1L;
              } else {
                  numEnt = numEnt64;
              }
  
              final long cenSize64 = zipFileSliceReader.getLong(eocdPos64 + 40);
              if (cenSize != cenSize64 && cenSize != 0xffffffff) {
                  throw new IOException(
                          "Mismatch in central directory size: " + cenSize + " vs. " + cenSize64 + ": " + getPath());
              }
              cenSize = cenSize64;
  
              // Recalculate the central directory position
              cenPos = eocdPos64 - cenSize;
  
              final long cenOff64 = zipFileSliceReader.getLong(eocdPos64 + 48);
              if (cenOff != cenOff64 && cenOff != 0xffffffff) {
                  throw new IOException(
                          "Mismatch in central directory offset: " + cenOff + " vs. " + cenOff64 + ": " + getPath());
              }
              cenOff = cenOff64;
          }
  
          // Get offset of first local file header
          final long locPos = cenPos - cenOff;
          if (locPos < 0) {
              throw new IOException("Local file header offset out of range: " + locPos + ": " + getPath());
          }
  
          // Read entries into a byte array, if central directory is smaller than 2GB. If central directory
          // is larger than 2GB, need to read each entry field from the file directly using ZipFileSliceReader.
          final byte[] entryBytes = cenSize > FileUtils.MAX_BUFFER_SIZE ? null : new byte[(int) cenSize];
          if (entryBytes != null) {
              zipFileSliceReader.read(cenPos, entryBytes, 0, (int) cenSize);
          }
  
          if (numEnt == -1L) {
              // numEnt and numEnt64 were inconsistent -- manually count entries
              numEnt = 0;
              for (long entOff = 0; entOff + 46 <= cenSize;) {
                  final int sig = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff)
                          : zipFileSliceReader.getInt(cenPos + entOff);
                  if (sig != 0x02014b50) {
                      throw new IOException("Invalid central directory signature: 0x" + Integer.toString(sig, 16)
                              + ": " + getPath());
                  }
                  final int filenameLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 28)
                          : zipFileSliceReader.getShort(cenPos + entOff + 28);
                  final int extraFieldLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 30)
                          : zipFileSliceReader.getShort(cenPos + entOff + 30);
                  final int commentLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 32)
                          : zipFileSliceReader.getShort(cenPos + entOff + 32);
                  entOff += 46 + filenameLen + extraFieldLen + commentLen;
                  numEnt++;
              }
          }
  
          //  Can't have more than (Integer.MAX_VALUE - 8) entries, since they are stored in an ArrayList
          if (numEnt > FileUtils.MAX_BUFFER_SIZE) {
              // One alternative in this (impossibly rare) situation would be to return only the first 2B entries
              throw new IOException("Too many zipfile entries: " + numEnt);
          }
  
          // Make sure there's no DoS attack vector by using a fake number of entries
          if (entryBytes != null && numEnt > entryBytes.length / 46) {
              // The smallest directory entry is 46 bytes in size
              throw new IOException("Too many zipfile entries: " + numEnt + " (expected a max of "
                      + entryBytes.length / 46 + " based on central directory size)");
          }
  
          // Enumerate entries
          entries = new ArrayList<>((int) numEnt);
          FastZipEntry manifestZipEntry = null;
          try {
              int entSize = 0;
              for (long entOff = 0; entOff + 46 <= cenSize; entOff += entSize) {
                  final int sig = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff)
                          : zipFileSliceReader.getInt(cenPos + entOff);
                  if (sig != 0x02014b50) {
                      throw new IOException("Invalid central directory signature: 0x" + Integer.toString(sig, 16)
                              + ": " + getPath());
                  }
                  final int filenameLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 28)
                          : zipFileSliceReader.getShort(cenPos + entOff + 28);
                  final int extraFieldLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 30)
                          : zipFileSliceReader.getShort(cenPos + entOff + 30);
                  final int commentLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 32)
                          : zipFileSliceReader.getShort(cenPos + entOff + 32);
                  entSize = 46 + filenameLen + extraFieldLen + commentLen;
  
                  // Get and sanitize entry name
                  final long filenameStartOff = entOff + 46;
                  final long filenameEndOff = filenameStartOff + filenameLen;
                  if (filenameEndOff > cenSize) {
                      if (log != null) {
                          log.log("Filename extends past end of entry -- skipping entry at offset " + entOff);
                      }
                      break;
                  }
                  final String entryName = entryBytes != null
                          ? ZipFileSliceReader.getString(entryBytes, filenameStartOff, filenameLen)
                          : zipFileSliceReader.getString(cenPos + filenameStartOff, filenameLen);
                  final String entryNameSanitized = FileUtils.sanitizeEntryPath(entryName,
                          /* removeInitialSlash = */ true);
                  if (entryNameSanitized.isEmpty() || entryName.endsWith("/")) {
                      // Skip directory entries
                      continue;
                  }
  
                  // Check entry flag bits
                  final int flags = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 8)
                          : zipFileSliceReader.getShort(cenPos + entOff + 8);
                  if ((flags & 1) != 0) {
                      if (log != null) {
                          log.log("Skipping encrypted zip entry: " + entryNameSanitized);
                      }
                      continue;
                  }
  
                  // Check compression method
                  final int compressionMethod = entryBytes != null
                          ? ZipFileSliceReader.getShort(entryBytes, entOff + 10)
                          : zipFileSliceReader.getShort(cenPos + entOff + 10);
                  if (compressionMethod != /* stored */ 0 && compressionMethod != /* deflated */ 8) {
                      if (log != null) {
                          log.log("Skipping zip entry with invalid compression method " + compressionMethod + ": "
                                  + entryNameSanitized);
                      }
                      continue;
                  }
                  final boolean isDeflated = compressionMethod == /* deflated */ 8;
  
                  // Get compressed and uncompressed size
                  long compressedSize = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff + 20)
                          : zipFileSliceReader.getInt(cenPos + entOff + 20);
                  long uncompressedSize = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff + 24)
                          : zipFileSliceReader.getInt(cenPos + entOff + 24);
                  long pos = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff + 42)
                          : zipFileSliceReader.getInt(cenPos + entOff + 42);
  
                  // Check for Zip64 header in extra fields
                  if (extraFieldLen > 0) {
                      for (int extraFieldOff = 0; extraFieldOff + 4 < extraFieldLen;) {
                          final long tagOff = filenameEndOff + extraFieldOff;
                          final int tag = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, tagOff)
                                  : zipFileSliceReader.getShort(cenPos + tagOff);
                          final int size = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, tagOff + 2)
                                  : zipFileSliceReader.getShort(cenPos + tagOff + 2);
                          if (extraFieldOff + 4 + size > extraFieldLen) {
                              // Invalid size
                              break;
                          }
                          if (tag == /* EXTID_ZIP64 */ 1 && size >= 24) {
                              final long uncompressedSizeL = entryBytes != null
                                      ? ZipFileSliceReader.getLong(entryBytes, tagOff + 4 + 0)
                                      : zipFileSliceReader.getLong(cenPos + tagOff + 4 + 0);
                              if (uncompressedSize == 0xffffffff) {
                                  uncompressedSize = uncompressedSizeL;
                              }
                              final long compressedSizeL = entryBytes != null
                                      ? ZipFileSliceReader.getLong(entryBytes, tagOff + 4 + 8)
                                      : zipFileSliceReader.getLong(cenPos + tagOff + 4 + 8);
                              if (compressedSize == 0xffffffff) {
                                  compressedSize = compressedSizeL;
                              }
                              final long posL = entryBytes != null
                                      ? ZipFileSliceReader.getLong(entryBytes, tagOff + 4 + 16)
                                      : zipFileSliceReader.getLong(cenPos + tagOff + 4 + 16);
                              if (pos == 0xffffffff) {
                                  pos = posL;
                              }
                              break;
                          }
                          extraFieldOff += 4 + size;
                      }
                  }
  
                  if (compressedSize < 0 || pos < 0) {
                      continue;
                  }
  
                  final long locHeaderPos = locPos + pos;
                  if (locHeaderPos < 0) {
                      if (log != null) {
                          log.log("Skipping zip entry with invalid loc header position: " + entryNameSanitized);
                      }
                      continue;
                  }
                  if (locHeaderPos + 4 >= len) {
                      if (log != null) {
                          log.log("Unexpected EOF when trying to read LOC header: " + entryNameSanitized);
                      }
                      continue;
                  }
  
                  // Add zip entry
                  final FastZipEntry entry = new FastZipEntry(this, locHeaderPos, entryNameSanitized, isDeflated,
                          compressedSize, uncompressedSize, physicalZipFile.nestedJarHandler);
                  entries.add(entry);
  
                  // Record manifest entry
                  if (entry.entryName.equals(MANIFEST_PATH)) {
                      manifestZipEntry = entry;
                  }
              }
          } catch (EOFException | IndexOutOfBoundsException e) {
              // Stop reading entries if any entry is not within file
              if (log != null) {
                  log.log("Reached premature EOF"
                          + (entries.isEmpty() ? "" : " after reading zip entry " + entries.get(entries.size() - 1)));
              }
          }
  
          // Parse manifest file, if present
          if (manifestZipEntry != null) {
              parseManifest(manifestZipEntry, log);
          }
  
          // For multi-release jars, drop any older or non-versioned entries that are masked by the most recent
          // version-specific entry
          if (isMultiReleaseJar) {
              if (VersionFinder.JAVA_MAJOR_VERSION < 9) {
                  if (log != null) {
                      log.log("This is a multi-release jar, but JRE version " + VersionFinder.JAVA_MAJOR_VERSION
                              + " does not support multi-release jars");
                  }
              } else {
                  if (log != null) {
                      // Find all the unique multirelease versions within the jar
                      final Set<Integer> versionsFound = new HashSet<>();
                      for (final FastZipEntry entry : entries) {
                          if (entry.version > 8) {
                              versionsFound.add(entry.version);
                          }
                      }
                      final List<Integer> versionsFoundSorted = new ArrayList<>(versionsFound);
                      CollectionUtils.sortIfNotEmpty(versionsFoundSorted);
                      log.log("This is a multi-release jar, with versions: " + Join.join(", ", versionsFoundSorted));
                  }
  
                  // Sort in decreasing order of version in preparation for version masking
                  CollectionUtils.sortIfNotEmpty(entries);
  
                  // Mask files that appear in multiple version sections, so that there is only one entry
                  // for each unversioned path, i.e. the versioned path with the highest version number
                  final List<FastZipEntry> unversionedZipEntriesMasked = new ArrayList<>(entries.size());
                  final Map<String, String> unversionedPathToVersionedPath = new HashMap<>();
                  for (final FastZipEntry versionedZipEntry : entries) {
                      if (!unversionedPathToVersionedPath.containsKey(versionedZipEntry.entryNameUnversioned)) {
                          // This is the first FastZipEntry for this entry's unversioned path
                          unversionedPathToVersionedPath.put(versionedZipEntry.entryNameUnversioned,
                                  versionedZipEntry.entryName);
                          unversionedZipEntriesMasked.add(versionedZipEntry);
                      } else if (log != null) {
                          log.log(unversionedPathToVersionedPath.get(versionedZipEntry.entryNameUnversioned)
                                  + " masks " + versionedZipEntry.entryName);
                      }
                  }
  
                  // Override entries with version-masked entries
                  entries = unversionedZipEntriesMasked;
              }
          }
      }