| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Alexander Akait @alexander-akait
- */
- "use strict";
- const { dirname, join } = require("./fs");
- /** @import { InputFileSystem } from "./fs" */
- /** @typedef {[string, string]} CaseCorrection wrongly cased segment paired with its real name */
- /** @typedef {{ corrections: CaseCorrection[], path: string }} CaseMismatch */
- /** Bounds the walk for a path pointing far outside any existing directory. */
- const MAX_MISSING_SEGMENTS = 20;
- /**
- * @param {InputFileSystem} fs input file system
- * @param {string} absolutePath absolute path
- * @returns {string} the last segment of the path, or an empty string at the root
- */
- const basename = (fs, absolutePath) => {
- const parent = dirname(fs, absolutePath);
- if (parent === absolutePath) return "";
- let start = parent.length;
- const charCode = absolutePath.charCodeAt(start);
- if (charCode === 47 /* "/" */ || charCode === 92 /* "\\" */) start++;
- return absolutePath.slice(start);
- };
- /**
- * Finds an existing path that differs from `missingPath` only in the casing of
- * its segments, by listing the directories the path walks through.
- * @param {InputFileSystem} fs input file system
- * @param {string} missingPath absolute path that does not exist
- * @param {(mismatch?: CaseMismatch) => void} callback receives the real path and the wrongly cased segments, outermost first, or nothing when the path does not exist under any casing
- * @returns {void}
- */
- const findCaseMismatch = (fs, missingPath, callback) => {
- /** @type {string[]} */
- const segments = [];
- /** @type {CaseCorrection[]} */
- const corrections = [];
- /**
- * @param {string} directory existing directory the remaining segments start from
- * @param {string[]} entries its entries
- * @param {number} index index into `segments`
- * @returns {void}
- */
- const walkDown = (directory, entries, index) => {
- const name = segments[index];
- let realName = name;
- if (!entries.includes(name)) {
- const lowerCasedName = name.toLowerCase();
- const matches = entries.filter((e) => e.toLowerCase() === lowerCasedName);
- // More than one match means the real name is not knowable from the request alone
- if (matches.length !== 1) return callback();
- realName = matches[0];
- corrections.push([name, realName]);
- }
- const next = join(fs, directory, realName);
- if (index === segments.length - 1) {
- return callback(
- corrections.length > 0 ? { corrections, path: next } : undefined
- );
- }
- fs.readdir(next, (err, nextEntries) => {
- if (err || !nextEntries) return callback();
- walkDown(next, /** @type {string[]} */ (nextEntries), index + 1);
- });
- };
- let current = missingPath;
- /**
- * @returns {void}
- */
- const walkUp = () => {
- const parent = dirname(fs, current);
- const name = basename(fs, current);
- if (parent === current || name === "") return callback();
- segments.push(name);
- if (segments.length > MAX_MISSING_SEGMENTS) return callback();
- fs.readdir(parent, (err, entries) => {
- if (err || !entries) {
- current = parent;
- return walkUp();
- }
- segments.reverse();
- walkDown(parent, /** @type {string[]} */ (entries), 0);
- });
- };
- walkUp();
- };
- /**
- * Rewrites a request with the real casing found on disk. Corrections are
- * case-only, so each one keeps the length and position of what it replaces —
- * a request that omitted the extension spells a prefix of the segment.
- * @param {string} request request as written
- * @param {CaseCorrection[]} corrections corrections, outermost first
- * @returns {string | undefined} the corrected request, or undefined when a correction is not spelled out in the request
- */
- const applyCaseCorrections = (request, corrections) => {
- let result = request;
- let cursor = 0;
- for (const [wrongName, realName] of corrections) {
- let index = result.indexOf(wrongName, cursor);
- let length = wrongName.length;
- if (index === -1) {
- length = 0;
- for (let i = wrongName.length - 1; i > 0; i--) {
- // A prefix that is already correctly cased identifies nothing — the
- // request would come back unchanged and the hint would repeat it
- if (
- result.endsWith(wrongName.slice(0, i)) &&
- realName.slice(0, i) !== wrongName.slice(0, i)
- ) {
- index = result.length - i;
- length = i;
- break;
- }
- }
- if (length === 0) return undefined;
- }
- result =
- result.slice(0, index) +
- realName.slice(0, length) +
- result.slice(index + length);
- cursor = index + length;
- }
- return result;
- };
- module.exports.applyCaseCorrections = applyCaseCorrections;
- module.exports.findCaseMismatch = findCaseMismatch;
|