diff --git a/webAO/client/fetchLists.ts b/webAO/client/fetchLists.ts index 9efd1817..a751ec07 100644 --- a/webAO/client/fetchLists.ts +++ b/webAO/client/fetchLists.ts @@ -1,6 +1,7 @@ import { client } from "../client"; import { AO_HOST } from "./aoHost"; import { request } from "../services/request.js"; +import { canonicalizePath } from "../utils/paths"; export const fetchBackgroundList = async () => { try { @@ -27,7 +28,7 @@ export const fetchCharacterList = async () => { char_select.innerHTML = ""; char_select.add(new Option("Custom", "0")); - + try { const chardata = await request(`${AO_HOST}characters.json`); const char_array = JSON.parse(chardata); @@ -58,7 +59,7 @@ export const fetchEvidenceList = async () => { evi_array.forEach((evi: string) => { evi_select.add(new Option(evi)); }); - + } catch (err) { console.warn("there was no evidence.json file"); } @@ -68,10 +69,15 @@ export const fetchEvidenceList = async () => { export const fetchManifest = async () => { try { const manifestdata = await request(`${AO_HOST}manifest.txt`); - client.manifest = manifestdata.split(/\r\n|\n\r|\n|\r/); + const tmp: string[] = manifestdata + .replace("\\", "/") + .split(/\r\n|\n\r|\n|\r/); + client.manifest = tmp.map(x => canonicalizePath(x)).sort(); + // the try catch will fail before here when there is no file } catch (err) { - console.warn("there was no manifest.txt file"); + console.warn("no manifest.txt, webao will be slow"); + client.manifest = []; } -} \ No newline at end of file +} diff --git a/webAO/client/handleCharacterInfo.ts b/webAO/client/handleCharacterInfo.ts index 9d74a8bc..f2d9bfcb 100644 --- a/webAO/client/handleCharacterInfo.ts +++ b/webAO/client/handleCharacterInfo.ts @@ -2,7 +2,7 @@ import { client } from "../client"; import { safeTags } from "../encoding"; import iniParse from "../iniParse"; import request from "../services/request"; -import fileExists from "../utils/fileExists"; +import { fileExistsManifest } from "../utils/fileExists"; import { AO_HOST } from "./aoHost"; @@ -23,7 +23,7 @@ export const handleCharacterInfo = async (chargs: string[], charid: number) => { )}/char_icon`; for (let i = 0; i < extensions.length; i++) { const fileUrl = charIconBaseUrl + extensions[i]; - const exists = await fileExists(fileUrl); + const exists = await fileExistsManifest(fileUrl); if (exists) { img.alt = chargs[0]; img.title = chargs[0]; @@ -102,4 +102,4 @@ export const handleCharacterInfo = async (chargs: string[], charid: number) => { console.warn(`missing charid ${charid}`); img.style.display = "none"; } -} \ No newline at end of file +} diff --git a/webAO/client/setEmote.ts b/webAO/client/setEmote.ts index f4fbdbb1..41fe6b51 100644 --- a/webAO/client/setEmote.ts +++ b/webAO/client/setEmote.ts @@ -1,6 +1,6 @@ import Client from "../client"; import transparentPng from "../constants/transparentPng"; -import fileExists from "../utils/fileExists"; +import {fileExistsManifest} from "../utils/fileExists"; /** * Sets all the img tags to the right sources @@ -39,13 +39,14 @@ const setEmote = async ( } else if (extension === ".webp.static") { url = `${characterFolder}${encodeURI(charactername)}/${encodeURI( emotename - )}.webp`; + )}.webp`; } else { url = `${characterFolder}${encodeURI(charactername)}/${encodeURI( prefix )}${encodeURI(emotename)}${extension}`; } - const exists = await fileExists(url); + + const exists = await fileExistsManifest(url); if (exists) { emoteSelector.src = url; break; diff --git a/webAO/packets/handlers/handlePV.ts b/webAO/packets/handlers/handlePV.ts index 3c669b96..affabbed 100644 --- a/webAO/packets/handlers/handlePV.ts +++ b/webAO/packets/handlers/handlePV.ts @@ -1,5 +1,5 @@ import { client } from "../../client"; -import fileExists from "../../utils/fileExists"; +import { fileExistsManifest } from "../../utils/fileExists"; import { updateActionCommands } from '../../dom/updateActionCommands' import { pickEmotion } from '../../dom/pickEmotion' import { AO_HOST } from "../../client/aoHost"; @@ -73,8 +73,8 @@ export const handlePV = async (args: string[]) => { } if ( - await fileExists( - `${AO_HOST}characters/${encodeURI(me.name.toLowerCase())}/custom.gif` + await fileExistsManifest( + `characters/${encodeURI(me.name.toLowerCase())}/custom.gif` ) ) { document.getElementById("button_4")!.style.display = ""; diff --git a/webAO/utils/binarySearch.js b/webAO/utils/binarySearch.js new file mode 100644 index 00000000..7ffbf1a1 --- /dev/null +++ b/webAO/utils/binarySearch.js @@ -0,0 +1,21 @@ +/* Returns the index of `needle' in `haystack', or `null' if it wasn't found. */ +export function binarySearch(haystack, + needle, + cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0) { + let start = 0; + let end = haystack.length - 1; + + while (start <= end) { + const middle = Math.floor((start + end) / 2); + const comparison = cmp(haystack[middle], needle); + + if (comparison == 0) + return middle; + else if (comparison < 0) + start = middle + 1; + else + end = middle - 1; + } + + return null; +} diff --git a/webAO/utils/fileExists.js b/webAO/utils/fileExists.js index 7978cbc6..dab41072 100644 --- a/webAO/utils/fileExists.js +++ b/webAO/utils/fileExists.js @@ -1,3 +1,8 @@ +import {AO_HOST} from "../client/aoHost" +import {client} from "../client" +import {canonicalizePath} from "./paths" +import {binarySearch} from "./binarySearch" + const fileExists = async (url) => new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('HEAD', url); @@ -16,3 +21,22 @@ const fileExists = async (url) => new Promise((resolve, reject) => { xhr.send(null); }); export default fileExists; + +/* Returns whether file exists. + * `url' is a URL, including the base URL for bw compat. + * If manifest is empty, check the old way. + * Otherwise, look it up in the manifest */ +const fileExistsManifest = async (url) => + new Promise((resolve, reject) => { + if(client.manifest.length == 0) { + resolve(fileExists(url)); + return; + } + const c_url = encodeURI(canonicalizePath(decodeURI(url.slice(AO_HOST.length)))); + + if(binarySearch(client.manifest, c_url) != null) + resolve(true); + + resolve(false); + }); +export {fileExistsManifest}; diff --git a/webAO/utils/getAnimLength.js b/webAO/utils/getAnimLength.js index aa303cfb..4eae58fa 100644 --- a/webAO/utils/getAnimLength.js +++ b/webAO/utils/getAnimLength.js @@ -1,5 +1,7 @@ +import {client} from '../client' +import {AO_HOST} from '../client/aoHost' import calculatorHandler from './calculatorHandler'; -import fileExists from './fileExists.js'; +import {fileExistsManifest} from './fileExists.js'; import { requestBuffer } from '../services/request.js'; /** * Gets animation length. If the animation cannot be found, it will @@ -11,7 +13,7 @@ const getAnimLength = async (url) => { const extensions = ['.gif', '.webp', '.apng']; for (const extension of extensions) { const urlWithExtension = url + extension; - const exists = await fileExists(urlWithExtension); + const exists = await fileExistsManifest(urlWithExtension); if (exists) { const fileBuffer = await requestBuffer(urlWithExtension); const length = calculatorHandler[extension](fileBuffer); diff --git a/webAO/utils/paths.ts b/webAO/utils/paths.ts index f4284b66..9cf4f197 100644 --- a/webAO/utils/paths.ts +++ b/webAO/utils/paths.ts @@ -1 +1,20 @@ export const getFilenameFromPath = (path: string) => path.substring(path.lastIndexOf('/') + 1) + +/* Removes `/./`, `/../`, and `//`. + * Does not add a leading `/` or `./`. + * Does not add a trailing `/`. */ +export function canonicalizePath(path: string): string { + const path_elements = path.split("/"); + var result: string[] = []; + + for (const el of path_elements) { + if (el === "..") + result.pop(); + else if (el === "." || el === "") + continue; + + result.push(el); + } + + return result.join("/"); +} diff --git a/webAO/utils/tryUrls.ts b/webAO/utils/tryUrls.ts index 14ef8852..1e5107f7 100644 --- a/webAO/utils/tryUrls.ts +++ b/webAO/utils/tryUrls.ts @@ -1,4 +1,4 @@ -import fileExists from './fileExists' +import {fileExistsManifest} from './fileExists' import transparentPng from '../constants/transparentPng' const urlExtensionsToTry = [ '.png', @@ -10,11 +10,11 @@ const tryUrls = async (url: string) => { for (let i = 0; i < urlExtensionsToTry.length; i++) { const extension = urlExtensionsToTry[i] const fullFileUrl = url + extension - const exists = await fileExists(fullFileUrl); + const exists = await fileExistsManifest(fullFileUrl); if (exists) { return fullFileUrl } } return transparentPng } -export default tryUrls \ No newline at end of file +export default tryUrls diff --git a/webAO/viewport/utils/setSide.ts b/webAO/viewport/utils/setSide.ts index 3726e83d..69018227 100644 --- a/webAO/viewport/utils/setSide.ts +++ b/webAO/viewport/utils/setSide.ts @@ -2,7 +2,7 @@ import { positions } from '../constants/positions' import { AO_HOST } from '../../client/aoHost' import { client } from '../../client' import tryUrls from '../../utils/tryUrls'; -import fileExists from '../../utils/fileExists'; +import { fileExistsManifest } from '../../utils/fileExists'; /** * Changes the viewport background based on a given position. @@ -57,10 +57,12 @@ export const set_side = async ({ } else { court.src = await tryUrls(client.viewport.getBackgroundFolder() + bg); } - - + + if (showDesk === true && desk) { - const deskFilename = (await fileExists(client.viewport.getBackgroundFolder() + desk.ao2)) + const deskFilename = (await fileExistsManifest( + (client.viewport.getBackgroundFolder() + + desk.ao2))) ? desk.ao2 : desk.ao1; bench.src = client.viewport.getBackgroundFolder() + deskFilename;