diff --git a/app.ts b/app.ts index 96b980ed5..7f735431e 100755 --- a/app.ts +++ b/app.ts @@ -45,7 +45,6 @@ import urljoin from 'url-join'; import * as aws from './lib/aws.js'; import * as normalizer from './lib/clientstate-normalizer.js'; -import {ElementType} from './lib/common-utils.js'; import {CompilationEnvironment} from './lib/compilation-env.js'; import {CompilationQueue} from './lib/compilation-queue.js'; import {CompilerFinder} from './lib/compiler-finder.js'; @@ -68,17 +67,24 @@ import {sources} from './lib/sources/index.js'; import {loadSponsorsFromString} from './lib/sponsors.js'; import {getStorageTypeByKey} from './lib/storage/index.js'; import * as utils from './lib/utils.js'; +import {ElementType} from './shared/common-utils.js'; import type {Language, LanguageKey} from './types/languages.interfaces.js'; // Used by assert.ts global.ce_base_directory = new URL('.', import.meta.url); +(nopt as any).invalidHandler = (key, val, types) => { + logger.error( + `Command line argument type error for "--${key}=${val}", expected ${types.map(t => typeof t).join(' | ')}`, + ); +}; + // Parse arguments from command line 'node ./app.js args...' const opts = nopt({ env: [String, Array], rootDir: [String], host: [String], - port: [String, Number], + port: [Number], propDebug: [Boolean], debug: [Boolean], dist: [Boolean], @@ -104,7 +110,33 @@ const opts = nopt({ version: [Boolean], webpackContent: [String], noLocal: [Boolean], -}); +}) as Partial<{ + env: string[]; + rootDir: string; + host: string; + port: number; + propDebug: boolean; + debug: boolean; + dist: boolean; + archivedVersions: string; + noRemoteFetch: boolean; + tmpDir: string; + wsl: boolean; + language: string; + noCache: boolean; + ensureNoIdClash: boolean; + logHost: string; + logPort: number; + hostnameForLogging: string; + suppressConsoleLog: boolean; + metricsPort: number; + loki: string; + discoveryonly: string; + prediscovered: string; + version: boolean; + webpackContent: string; + noLocal: boolean; +}>; if (opts.debug) logger.level = 'debug'; @@ -158,8 +190,22 @@ const releaseBuildNumber = (() => { return ''; })(); +export type AppDefaultArguments = { + rootDir: string; + env: string[]; + hostname?: string; + port: number; + gitReleaseName: string; + releaseBuildNumber: string; + wantedLanguages: string | null; + doCache: boolean; + fetchCompilersFromRemote: boolean; + ensureNoCompilerClash: boolean | undefined; + suppressConsoleLog: boolean; +}; + // Set default values for omitted arguments -const defArgs = { +const defArgs: AppDefaultArguments = { rootDir: opts.rootDir || './etc', env: opts.env || ['dev'], hostname: opts.host, @@ -221,7 +267,7 @@ props.initialize(configDir, propHierarchy); // Instantiate a function to access records concerning "compiler-explorer" // in hidden object props.properties const ceProps = props.propsFor('compiler-explorer'); -defArgs.wantedLanguages = ceProps('restrictToLanguages', defArgs.wantedLanguages); +defArgs.wantedLanguages = ceProps('restrictToLanguages', defArgs.wantedLanguages); const languages = (() => { if (defArgs.wantedLanguages) { @@ -229,7 +275,7 @@ const languages = (() => { const passedLangs = defArgs.wantedLanguages.split(','); for (const wantedLang of passedLangs) { for (const lang of Object.values(allLanguages)) { - if (lang.id === wantedLang || lang.name === wantedLang || lang.alias === wantedLang) { + if (lang.id === wantedLang || lang.name === wantedLang || lang.alias.includes(wantedLang)) { filteredLangs[lang.id] = lang; } } @@ -439,7 +485,12 @@ function startListening(server: express.Express) { logger.info(` Listening on http://${defArgs.hostname || 'localhost'}:${_port}/`); logger.info(` Startup duration: ${startupDurationMs}ms`); logger.info('======================================='); - server.listen(_port, defArgs.hostname); + // silly express typing, passing undefined is fine but + if (defArgs.hostname) { + server.listen(_port, defArgs.hostname); + } else { + server.listen(_port); + } } } diff --git a/docs/SupportedEmulators.md b/docs/SupportedEmulators.md index b99d316ae..8271baf6c 100644 --- a/docs/SupportedEmulators.md +++ b/docs/SupportedEmulators.md @@ -4,11 +4,15 @@ These are using Javascript and/or using external websites to facilitate emulation after creating a suitable binary. -- [NES](https://github.com/compiler-explorer/jsnes-ceweb) (https://static.ce-cdn.net/jsnes-ceweb/index.html) - for images built with LLVM MOS NES or CC65 (`--target nes`) +- [NES](https://github.com/compiler-explorer/jsnes-ceweb) (https://static.ce-cdn.net/jsnes-ceweb/index.html) - for + images built with LLVM MOS NES or CC65 (`--target nes`) - [JSBeeb](https://github.com/mattgodbolt/jsbeeb) (https://bbc.godbolt.org) - for binaries built with BeebAsm -- [Speccy](https://github.com/compiler-explorer/jsspeccy3) (https://static.ce-cdn.net/jsspeccy/index.html) - for `.tap` files built with Z88DK (target `+zx`) -- [Miracle](https://github.com/mattgodbolt/Miracle) (https://xania.org/miracle/miracle.html) - for `.sms` files built with Z88DK (target `+sms`) -- [Viciious](https://github.com/compiler-explorer/viciious) (https://static.ce-cdn.net/viciious/viciious.html) - for `.prg` files built with LLVM MOS C64 or CC65 (`--target c64`) +- [Speccy](https://github.com/compiler-explorer/jsspeccy3) (https://static.ce-cdn.net/jsspeccy/index.html) - for `.tap` + files built with Z88DK (target `+zx`) +- [Miracle](https://github.com/mattgodbolt/Miracle) (https://xania.org/miracle/miracle.html) - for `.sms` files built + with Z88DK (target `+sms`) +- [Viciious](https://github.com/compiler-explorer/viciious) (https://static.ce-cdn.net/viciious/viciious.html) - for + `.prg` files built with LLVM MOS C64 or CC65 (`--target c64`) ## Examples diff --git a/lib/assert.ts b/lib/assert.ts index 658034bb7..8768ddecb 100644 --- a/lib/assert.ts +++ b/lib/assert.ts @@ -25,8 +25,8 @@ import * as fs from 'fs'; import path from 'path'; -import {parse} from './stacktrace.js'; -import {isString} from './common-utils.js'; +import {parse} from '../shared/stacktrace.js'; +import {isString} from '../shared/common-utils.js'; const filePrefix = 'file://'; diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts index 0f69187cb..0569481f6 100644 --- a/lib/base-compiler.ts +++ b/lib/base-compiler.ts @@ -37,6 +37,7 @@ import { CompilationCacheKey, CompilationInfo, CompilationResult, + CompileChildLibraries, CustomInputForTool, ExecutionOptions, bypassCompilationCache, @@ -105,6 +106,8 @@ import { } from '../types/compilation/compiler-overrides.interfaces.js'; import {LLVMIrBackendOptions} from '../types/compilation/ir.interfaces.js'; import {ParsedAsmResultLine} from '../types/asmresult/asmresult.interfaces.js'; +import {unique} from '../shared/common-utils.js'; +import {ClientOptionsType, OptionsHandlerLibrary, VersionInfo} from './options-handler.js'; const compilationTimeHistogram = new PromClient.Histogram({ name: 'ce_base_compiler_compilation_duration_seconds', @@ -261,8 +264,8 @@ export class BaseCompiler implements ICompiler { this.packager = new Packager(); } - copyAndFilterLibraries(allLibraries, filter) { - const filterLibAndVersion = _.map(filter, lib => { + copyAndFilterLibraries(allLibraries: Record, filter: string[]) { + const filterLibAndVersion = filter.map(lib => { const match = lib.match(/([\w-]*)\.([\w-]*)/i); if (match) { return { @@ -296,7 +299,7 @@ export class BaseCompiler implements ICompiler { } return true; - }); + }) as Record; copiedLibraries[libid] = libcopy; }); @@ -304,7 +307,7 @@ export class BaseCompiler implements ICompiler { return copiedLibraries; } - getSupportedLibraries(supportedLibrariesArr, allLibs) { + getSupportedLibraries(supportedLibrariesArr: string[], allLibs: Record) { if (supportedLibrariesArr.length > 0) { return this.copyAndFilterLibraries(allLibs, supportedLibrariesArr); } @@ -748,15 +751,15 @@ export class BaseCompiler implements ICompiler { }; } - getSortedStaticLibraries(libraries) { + getSortedStaticLibraries(libraries: CompileChildLibraries[]) { const dictionary = {}; - const links = _.uniq( - _.flatten( - _.map(libraries, selectedLib => { + const links = unique( + libraries + .map(selectedLib => { const foundVersion = this.findLibVersion(selectedLib); if (!foundVersion) return false; - return _.map(foundVersion.staticliblink, lib => { + return foundVersion.staticliblink.map(lib => { if (lib) { dictionary[lib] = foundVersion; return [lib, foundVersion.dependencies]; @@ -764,106 +767,105 @@ export class BaseCompiler implements ICompiler { return false; } }); - }), - ), + }) + .flat(3), ); const sortedlinks: string[] = []; - _.each(links, libToInsertName => { - const libToInsertObj = dictionary[libToInsertName]; + for (const libToInsertName of links) { + if (libToInsertName) { + const libToInsertObj = dictionary[libToInsertName]; - let idxToInsert = sortedlinks.length; - for (const [idx, libCompareName] of sortedlinks.entries()) { - const libCompareObj: LibraryVersion = dictionary[libCompareName]; + let idxToInsert = sortedlinks.length; + for (const [idx, libCompareName] of sortedlinks.entries()) { + const libCompareObj: LibraryVersion = dictionary[libCompareName]; - if ( - libToInsertObj && - libCompareObj && - _.intersection(libToInsertObj.dependencies, libCompareObj.staticliblink).length > 0 - ) { - idxToInsert = idx; - break; - } else if (libToInsertObj && libToInsertObj.dependencies.includes(libCompareName)) { - idxToInsert = idx; - break; - } else if (libCompareObj && libCompareObj.dependencies.includes(libToInsertName)) { - continue; - } else if ( - libToInsertObj && - libToInsertObj.staticliblink.includes(libToInsertName) && - libToInsertObj.staticliblink.includes(libCompareName) - ) { if ( - libToInsertObj.staticliblink.indexOf(libToInsertName) > - libToInsertObj.staticliblink.indexOf(libCompareName) + libToInsertObj && + libCompareObj && + _.intersection(libToInsertObj.dependencies, libCompareObj.staticliblink).length > 0 ) { - continue; - } else { idxToInsert = idx; - } - break; - } else if ( - libCompareObj && - libCompareObj.staticliblink.includes(libToInsertName) && - libCompareObj.staticliblink.includes(libCompareName) - ) { - if ( - libCompareObj.staticliblink.indexOf(libToInsertName) > - libCompareObj.staticliblink.indexOf(libCompareName) + break; + } else if (libToInsertObj && libToInsertObj.dependencies.includes(libCompareName)) { + idxToInsert = idx; + break; + } else if (libCompareObj && libCompareObj.dependencies.includes(libToInsertName)) { + continue; + } else if ( + libToInsertObj && + libToInsertObj.staticliblink.includes(libToInsertName) && + libToInsertObj.staticliblink.includes(libCompareName) ) { - continue; - } else { - idxToInsert = idx; + if ( + libToInsertObj.staticliblink.indexOf(libToInsertName) > + libToInsertObj.staticliblink.indexOf(libCompareName) + ) { + continue; + } else { + idxToInsert = idx; + } + break; + } else if ( + libCompareObj && + libCompareObj.staticliblink.includes(libToInsertName) && + libCompareObj.staticliblink.includes(libCompareName) + ) { + if ( + libCompareObj.staticliblink.indexOf(libToInsertName) > + libCompareObj.staticliblink.indexOf(libCompareName) + ) { + continue; + } else { + idxToInsert = idx; + } + break; } - break; + } + + if (idxToInsert < sortedlinks.length) { + sortedlinks.splice(idxToInsert, 0, libToInsertName); + } else { + sortedlinks.push(libToInsertName); } } - - if (idxToInsert < sortedlinks.length) { - sortedlinks.splice(idxToInsert, 0, libToInsertName); - } else { - sortedlinks.push(libToInsertName); - } - }); + } return sortedlinks; } - getStaticLibraryLinks(libraries) { + getStaticLibraryLinks(libraries: CompileChildLibraries[]) { const linkFlag = this.compiler.linkFlag || '-l'; - return _.map(this.getSortedStaticLibraries(libraries), lib => { - if (lib) { - return linkFlag + lib; - } else { - return false; - } - }) as string[]; + return this.getSortedStaticLibraries(libraries) + .filter(lib => lib) + .map(lib => linkFlag + lib); } - getSharedLibraryLinks(libraries: any[]): string[] { + getSharedLibraryLinks(libraries: CompileChildLibraries[]): string[] { const linkFlag = this.compiler.linkFlag || '-l'; - return _.flatten( - _.map(libraries, selectedLib => { + return libraries + .map(selectedLib => { const foundVersion = this.findLibVersion(selectedLib); if (!foundVersion) return false; - return _.map(foundVersion.liblink, lib => { + return foundVersion.liblink.map(lib => { if (lib) { return linkFlag + lib; } else { return false; } }); - }), - ) as string[]; + }) + .flat() + .filter(link => link) as string[]; } - getSharedLibraryPaths(libraries) { - return _.flatten( - _.map(libraries, selectedLib => { + getSharedLibraryPaths(libraries: CompileChildLibraries[]) { + return libraries + .map(selectedLib => { const foundVersion = this.findLibVersion(selectedLib); if (!foundVersion) return false; @@ -872,11 +874,15 @@ export class BaseCompiler implements ICompiler { paths.push(`/app/${selectedLib.id}/lib`); } return paths; - }), - ) as string[]; + }) + .flat(); } - protected getSharedLibraryPathsAsArguments(libraries, libDownloadPath?: string, toolchainPath?: string) { + protected getSharedLibraryPathsAsArguments( + libraries: CompileChildLibraries[], + libDownloadPath?: string, + toolchainPath?: string, + ) { const pathFlag = this.compiler.rpathFlag || '-Wl,-rpath,'; const libPathFlag = this.compiler.libpathFlag || '-L'; @@ -1058,7 +1064,7 @@ export class BaseCompiler implements ICompiler { backendOptions: Record, inputFilename: string, outputFilename: string, - libraries, + libraries: CompileChildLibraries[], overrides: ConfiguredOverrides, ) { let options = this.optionsForFilter(filters, outputFilename, userOptions); @@ -1086,9 +1092,9 @@ export class BaseCompiler implements ICompiler { let staticLibLinks: string[] = []; if (filters.binary) { - libLinks = this.getSharedLibraryLinks(libraries) || []; + libLinks = (this.getSharedLibraryLinks(libraries).filter(l => l) as string[]) || []; libPaths = this.getSharedLibraryPathsAsArguments(libraries, undefined, toolchainPath); - staticLibLinks = this.getStaticLibraryLinks(libraries) || []; + staticLibLinks = (this.getStaticLibraryLinks(libraries).filter(l => l) as string[]) || []; } userOptions = this.filterUserOptions(userOptions) || []; @@ -2041,7 +2047,16 @@ export class BaseCompiler implements ICompiler { } } - async doCompilation(inputFilename, dirPath, key, options, filters, backendOptions, libraries, tools) { + async doCompilation( + inputFilename, + dirPath, + key, + options, + filters, + backendOptions, + libraries: CompileChildLibraries[], + tools, + ) { const inputFilenameSafe = this.filename(inputFilename); const outputFilename = this.getOutputFilename(dirPath, this.outputFilebase, key); @@ -2565,7 +2580,7 @@ export class BaseCompiler implements ICompiler { bypassCache: BypassCache, tools, executionParameters, - libraries, + libraries: CompileChildLibraries[], files, ) { const optionsError = this.checkOptions(options); @@ -3093,8 +3108,13 @@ but nothing was dumped. Possible causes are: } } - initialiseLibraries(clientOptions) { - this.supportedLibraries = this.getSupportedLibraries(this.compiler.libsArr, clientOptions.libs[this.lang.id]); + initialiseLibraries(clientOptions: ClientOptionsType) { + // TODO: Awful cast here because of OptionsHandlerLibrary vs Library. These might really be the same types and + // OptionsHandlerLibrary should maybe be yeeted. + this.supportedLibraries = this.getSupportedLibraries( + this.compiler.libsArr, + clientOptions.libs[this.lang.id], + ) as any as Record; } async getTargetsAsOverrideValues(): Promise { @@ -3178,7 +3198,7 @@ but nothing was dumped. Possible causes are: return this.env.getPossibleToolchains(); } - async initialise(mtime: Date, clientOptions, isPrediscovered = false) { + async initialise(mtime: Date, clientOptions: ClientOptionsType, isPrediscovered = false) { this.mtime = mtime; if (this.buildenvsetup) { diff --git a/lib/compiler-finder.ts b/lib/compiler-finder.ts index 6a4a1b134..2e5d21f61 100644 --- a/lib/compiler-finder.ts +++ b/lib/compiler-finder.ts @@ -38,12 +38,13 @@ import {unwrap, assert} from './assert.js'; import {InstanceFetcher} from './aws.js'; import {CompileHandler} from './handlers/compile.js'; import {logger} from './logger.js'; -import {ClientOptionsHandler, OptionHandlerArguments} from './options-handler.js'; +import {ClientOptionsHandler} from './options-handler.js'; import {CompilerProps} from './properties.js'; import type {PropertyGetter} from './properties.interfaces.js'; -import {basic_comparator, remove} from './common-utils.js'; +import {basic_comparator, remove} from '../shared/common-utils.js'; import {getPossibleGccToolchainsFromCompilerInfo} from './toolchain-utils.js'; import {InstructionSet, InstructionSetsList} from '../types/instructionsets.js'; +import {AppDefaultArguments} from '../app.js'; const sleep = promisify(setTimeout); @@ -54,7 +55,7 @@ export class CompilerFinder { compilerProps: CompilerProps['get']; ceProps: PropertyGetter; awsProps: PropertyGetter; - args: OptionHandlerArguments; + args: AppDefaultArguments; compileHandler: CompileHandler; languages: Record; awsPoller: InstanceFetcher | null = null; @@ -64,7 +65,7 @@ export class CompilerFinder { compileHandler: CompileHandler, compilerProps: CompilerProps, awsProps: PropertyGetter, - args: OptionHandlerArguments, + args: AppDefaultArguments, optionsHandler: ClientOptionsHandler, ) { this.compilerProps = compilerProps.get.bind(compilerProps); diff --git a/lib/compilers/nvcc.ts b/lib/compilers/nvcc.ts index f55f9a231..75ba0c5ab 100644 --- a/lib/compilers/nvcc.ts +++ b/lib/compilers/nvcc.ts @@ -108,24 +108,24 @@ export class NvccCompiler extends BaseCompiler { filters.binary ? this.objdump(outputFilename, {}, maxSize, filters.intel, filters.demangle, false, false, filters) : (async () => { - if (result.asmSize === undefined) { - result.asm = ''; - return result; - } - if (result.asmSize >= maxSize) { - result.asm = - ' ${maxSize} bytes)>`; - return result; - } - if (postProcess.length > 0) { - return await this.execPostProcess(result, postProcess, outputFilename, maxSize); - } else { - const contents = await fs.readFile(outputFilename, {encoding: 'utf8'}); - result.asm = contents.toString(); - return result; - } - })() + if (result.asmSize === undefined) { + result.asm = ''; + return result; + } + if (result.asmSize >= maxSize) { + result.asm = + ' ${maxSize} bytes)>`; + return result; + } + if (postProcess.length > 0) { + return await this.execPostProcess(result, postProcess, outputFilename, maxSize); + } else { + const contents = await fs.readFile(outputFilename, {encoding: 'utf8'}); + result.asm = contents.toString(); + return result; + } + })() ).then(asm => { result.asm = typeof asm === 'string' ? asm : asm.asm; return result; diff --git a/lib/handlers/api.ts b/lib/handlers/api.ts index 659dad09e..d3b280b78 100644 --- a/lib/handlers/api.ts +++ b/lib/handlers/api.ts @@ -30,7 +30,7 @@ import {CompilerInfo} from '../../types/compiler.interfaces.js'; import {Language, LanguageKey} from '../../types/languages.interfaces.js'; import {assert, unwrap} from '../assert.js'; import {ClientStateNormalizer} from '../clientstate-normalizer.js'; -import {isString, unique} from '../common-utils.js'; +import {isString, unique} from '../../shared/common-utils.js'; import {logger} from '../logger.js'; import {ClientOptionsHandler} from '../options-handler.js'; import {PropertyGetter} from '../properties.interfaces.js'; @@ -241,9 +241,10 @@ export class ApiHandler { return Object.keys(libsForLanguageObj).map(key => { const language = libsForLanguageObj[key]; const versionArr = Object.keys(language.versions).map(key => { - const versionObj = Object.assign({}, language.versions[key]); - versionObj.id = key; - return versionObj; + return { + ...language.versions[key], + id: key, + }; }); return { diff --git a/lib/handlers/compile.ts b/lib/handlers/compile.ts index 19594f03b..9350a9878 100644 --- a/lib/handlers/compile.ts +++ b/lib/handlers/compile.ts @@ -48,11 +48,12 @@ import { CompileRequestTextBody, ExecutionRequestParams, } from './compile.interfaces.js'; -import {remove} from '../common-utils.js'; +import {remove} from '../../shared/common-utils.js'; import {CompilerOverrideOptions} from '../../types/compilation/compiler-overrides.interfaces.js'; import {BypassCache, CompileChildLibraries, ExecutionParams} from '../../types/compilation/compilation.interfaces.js'; import {SentryCapture} from '../sentry.js'; import {ResultLine} from '../../types/resultline/resultline.interfaces.js'; +import {ClientOptionsType} from '../options-handler.js'; temp.track(); @@ -101,7 +102,7 @@ export class CompileHandler { private readonly textBanner: string; private readonly proxy: Server; private readonly awsProps: PropertyGetter; - private clientOptions: Record | null = null; + private clientOptions: ClientOptionsType | null = null; private readonly compileCounter: Counter = new PromClient.Counter({ name: 'ce_compilations_total', help: 'Number of compilations', @@ -245,7 +246,7 @@ export class CompileHandler { } } - async setCompilers(compilers: PreliminaryCompilerInfo[], clientOptions: Record) { + async setCompilers(compilers: PreliminaryCompilerInfo[], clientOptions: ClientOptionsType) { // Be careful not to update this.compilersById until we can replace it entirely. const compilersById = {}; try { diff --git a/lib/handlers/noscript.ts b/lib/handlers/noscript.ts index 50fe5af13..d74521c97 100644 --- a/lib/handlers/noscript.ts +++ b/lib/handlers/noscript.ts @@ -28,7 +28,7 @@ import express from 'express'; import {assert} from '../assert.js'; import {ClientState} from '../clientstate.js'; import {ClientStateNormalizer} from '../clientstate-normalizer.js'; -import {isString} from '../common-utils.js'; +import {isString} from '../../shared/common-utils.js'; import {logger} from '../logger.js'; import {ClientOptionsHandler} from '../options-handler.js'; import {StorageBase} from '../storage/index.js'; diff --git a/lib/handlers/route-api.ts b/lib/handlers/route-api.ts index 608c46d14..d067aac62 100644 --- a/lib/handlers/route-api.ts +++ b/lib/handlers/route-api.ts @@ -27,7 +27,7 @@ import express from 'express'; import {assert, unwrap} from '../assert.js'; import {ClientState} from '../clientstate.js'; import {ClientStateGoldenifier, ClientStateNormalizer} from '../clientstate-normalizer.js'; -import {isString} from '../common-utils.js'; +import {isString} from '../../shared/common-utils.js'; import {logger} from '../logger.js'; import {StorageBase} from '../storage/index.js'; import * as utils from '../utils.js'; diff --git a/lib/languages.ts b/lib/languages.ts index 4c91280b1..4553f1642 100644 --- a/lib/languages.ts +++ b/lib/languages.ts @@ -25,7 +25,6 @@ import path from 'path'; import fs from 'fs-extra'; -import _ from 'underscore'; import type {Language, LanguageKey} from '../types/languages.interfaces.js'; @@ -684,19 +683,21 @@ const definitions: Record = { }, }; -export const languages: Record = _.mapObject(definitions, (lang, key) => { - let example: string; - try { - example = fs.readFileSync(path.join('examples', key, 'default' + lang.extensions[0]), 'utf8'); - } catch (error) { - example = 'Oops, something went wrong and we could not get the default code for this language.'; - } +export const languages = Object.fromEntries( + Object.entries(definitions).map(([key, lang]) => { + let example: string; + try { + example = fs.readFileSync(path.join('examples', key, 'default' + lang.extensions[0]), 'utf8'); + } catch (error) { + example = 'Oops, something went wrong and we could not get the default code for this language.'; + } - const def: Language = { - ...lang, - id: key as LanguageKey, - supportsExecute: false, - example, - }; - return def; -}); + const def: Language = { + ...lang, + id: key as LanguageKey, + supportsExecute: false, + example, + }; + return [key, def]; + }), +) as Record; diff --git a/lib/llvm-ir.ts b/lib/llvm-ir.ts index 2860b66b1..85a7b79b5 100644 --- a/lib/llvm-ir.ts +++ b/lib/llvm-ir.ts @@ -22,14 +22,13 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. -import _ from 'underscore'; - import type {IRResultLine} from '../types/asmresult/asmresult.interfaces.js'; import * as utils from './utils.js'; import {LLVMIrBackendOptions} from '../types/compilation/ir.interfaces.js'; import {LLVMIRDemangler} from './demangler/llvm.js'; import {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js'; +import {isString} from '../shared/common-utils.js'; type MetaNode = { metaId: string; @@ -246,7 +245,7 @@ export class LlvmIrParser { } async processFromFilters(ir, filters: ParseFiltersAndOutputOptions) { - if (_.isString(ir)) { + if (isString(ir)) { return await this.processIr(ir, { filterDebugInfo: !!filters.debugCalls, filterIRMetadata: !!filters.directives, diff --git a/lib/metrics-server.ts b/lib/metrics-server.ts index a6a555b70..2e12a7e3c 100644 --- a/lib/metrics-server.ts +++ b/lib/metrics-server.ts @@ -32,7 +32,7 @@ import PromClient from 'prom-client'; * @param hostname - The TCP host to attach the listener. * @returns void */ -export function setupMetricsServer(serverPort: number, hostname: string): void { +export function setupMetricsServer(serverPort: number, hostname: string | undefined): void { PromClient.collectDefaultMetrics(); const metricsServer = express(); @@ -45,5 +45,10 @@ export function setupMetricsServer(serverPort: number, hostname: string): void { .catch(err => res.status(500).send(err)); }); - metricsServer.listen(serverPort, hostname); + // silly express typing, passing undefined is fine but + if (hostname) { + metricsServer.listen(serverPort, hostname); + } else { + metricsServer.listen(serverPort); + } } diff --git a/lib/options-handler.ts b/lib/options-handler.ts index 318f10875..b30295aca 100755 --- a/lib/options-handler.ts +++ b/lib/options-handler.ts @@ -38,24 +38,37 @@ import type {PropertyGetter, PropertyValue} from './properties.interfaces.js'; import {Source} from './sources/index.js'; import {BaseTool, getToolTypeByKey} from './tooling/index.js'; import {asSafeVer, getHash, splitArguments, splitIntoArray} from './utils.js'; +import {AppDefaultArguments} from '../app.js'; -// TODO: There is surely a better name for this type. Used both here and in the compiler finder. -export type OptionHandlerArguments = { - rootDir: string; - env: string[]; - hostname: string[]; - port: number; - gitReleaseName: string; - releaseBuildNumber: string; - wantedLanguages: string | null; - doCache: boolean; - fetchCompilersFromRemote: boolean; - ensureNoCompilerClash: boolean; - suppressConsoleLog: boolean; +// TODO: Figure out if same as libraries.interfaces.ts? +export type VersionInfo = { + version: string; + staticliblink: string[]; + alias: string[]; + dependencies: string[]; + path: string[]; + libpath: string[]; + liblink: string[]; + lookupversion?: PropertyValue; + options: string[]; + hidden: boolean; + packagedheaders?: boolean; +}; +export type OptionsHandlerLibrary = { + name: string; + url: string; + description: string; + staticliblink: string[]; + liblink: string[]; + dependencies: string[]; + versions: Record; + examples: string[]; + options: string[]; + packagedheaders?: boolean; }; // TODO: Is this the same as Options in static/options.interfaces.ts? -type OptionsType = { +export type ClientOptionsType = { googleAnalyticsAccount: string; googleAnalyticsEnabled: boolean; sharingEnabled: boolean; @@ -66,7 +79,7 @@ type OptionsType = { urlShortenService: string; defaultSource: string; compilers: never[]; - libs: Record; + libs: Record>; remoteLibs: Record; tools: Record; defaultLibs: Record; @@ -119,7 +132,7 @@ export class ClientOptionsHandler { supportsLibraryCodeFilterPerLanguage: Record; supportsLibraryCodeFilter: boolean; remoteLibs: Record; - options: OptionsType; + options: ClientOptionsType; optionsJSON: string; optionsHash: string; /*** @@ -130,7 +143,7 @@ export class ClientOptionsHandler { * @param {CompilerProps} compilerProps * @param {Object} defArgs - Compiler Explorer arguments */ - constructor(fileSources: Source[], compilerProps: CompilerProps, defArgs: OptionHandlerArguments) { + constructor(fileSources: Source[], compilerProps: CompilerProps, defArgs: AppDefaultArguments) { this.compilerProps = compilerProps.get.bind(compilerProps); this.ceProps = compilerProps.ceProps; const ceProps = compilerProps.ceProps; @@ -262,33 +275,8 @@ export class ClientOptionsHandler { } parseLibraries(baseLibs: Record) { - type VersionInfo = { - version: string; - staticliblink: string[]; - alias: string[]; - dependencies: string[]; - path: string[]; - libpath: string[]; - liblink: string[]; - lookupversion?: PropertyValue; - options: string[]; - hidden: boolean; - packagedheaders?: boolean; - }; - type Library = { - name: string; - url: string; - description: string; - staticliblink: string[]; - liblink: string[]; - dependencies: string[]; - versions: Record; - examples: string[]; - options: string[]; - packagedheaders?: boolean; - }; // Record language -> {Record lib name -> lib} - const libraries: Record> = {}; + const libraries: Record> = {}; for (const [lang, forLang] of Object.entries(baseLibs)) { if (lang && forLang) { libraries[lang] = {}; diff --git a/lib/parsers/asm-parser.ts b/lib/parsers/asm-parser.ts index a22856276..f10d6a237 100644 --- a/lib/parsers/asm-parser.ts +++ b/lib/parsers/asm-parser.ts @@ -32,7 +32,7 @@ import { } from '../../types/asmresult/asmresult.interfaces.js'; import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js'; import {assert} from '../assert.js'; -import {isString} from '../common-utils.js'; +import {isString} from '../../shared/common-utils.js'; import {PropertyGetter} from '../properties.interfaces.js'; import * as utils from '../utils.js'; diff --git a/lib/properties.ts b/lib/properties.ts index 09b9d4533..78c92b537 100644 --- a/lib/properties.ts +++ b/lib/properties.ts @@ -32,6 +32,7 @@ import type {LanguageKey} from '../types/languages.interfaces.js'; import {logger} from './logger.js'; import type {PropertyGetter, PropertyValue, Widen} from './properties.interfaces.js'; import {toProperty} from './utils.js'; +import {isString} from '../shared/common-utils.js'; let properties: Record> = {}; @@ -94,7 +95,7 @@ export function parseProperties(blob: string, name) { return props; } -export function initialize(directory, hier) { +export function initialize(directory: string, hier) { if (hier === null) throw new Error('Must supply a hierarchy array'); hierarchy = _.map(hier, x => x.toLowerCase()); logger.info(`Reading properties from ${directory} with hierarchy ${hierarchy}`); @@ -258,7 +259,7 @@ export class CompilerProps { if (_.isEmpty(langs)) { return map_fn(this.ceProps(key, defaultValue)); } - if (_.isString(langs)) { + if (isString(langs)) { if (this.propsByLangId[langs]) { return map_fn(this.$getInternal(langs, key, defaultValue), this.languages[langs]); } else { diff --git a/lib/sentry.ts b/lib/sentry.ts index 01f94d3c6..ad2f15444 100644 --- a/lib/sentry.ts +++ b/lib/sentry.ts @@ -24,7 +24,7 @@ import {logger} from './logger.js'; import {PropertyGetter} from './properties.interfaces.js'; -import {parse} from './stacktrace.js'; +import {parse} from '../shared/stacktrace.js'; import * as Sentry from '@sentry/node'; diff --git a/package-lock.json b/package-lock.json index d8b0316cf..fbaf49ec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,6 +97,7 @@ "@types/js-cookie": "^3.0.2", "@types/mocha": "^10.0.1", "@types/node-targz": "^0.2.0", + "@types/nopt": "^3.0.29", "@types/qs": "^6.9.7", "@types/request": "^2.48.8", "@types/shell-quote": "^1.7.1", @@ -4420,6 +4421,12 @@ "@types/tar-fs": "*" } }, + "node_modules/@types/nopt": { + "version": "3.0.29", + "resolved": "https://registry.npmjs.org/@types/nopt/-/nopt-3.0.29.tgz", + "integrity": "sha512-PAO73Sc7+IiTIuPY1r/l+TgdIK4lugz5QxPaQ25EsjBBuZAw8OOtNEEGXvGciYwWa+JBE5wNQ8mR6nJE+H2csQ==", + "dev": true + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", diff --git a/package.json b/package.json index 952c591f5..e2bd41595 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "@types/js-cookie": "^3.0.2", "@types/mocha": "^10.0.1", "@types/node-targz": "^0.2.0", + "@types/nopt": "^3.0.29", "@types/qs": "^6.9.7", "@types/request": "^2.48.8", "@types/shell-quote": "^1.7.1", diff --git a/shared/.eslint-ce-lib.yml b/shared/.eslint-ce-lib.yml new file mode 100644 index 000000000..08fd04652 --- /dev/null +++ b/shared/.eslint-ce-lib.yml @@ -0,0 +1,94 @@ +--- +plugins: + - jsdoc + - sonarjs + - unicorn + - prettier +extends: + - ../.eslint-license-header.yml + - eslint:recommended +env: + browser: true + node: true + es6: false +rules: + comma-dangle: + - error + - arrays: always-multiline + objects: always-multiline + imports: always-multiline + exports: always-multiline + functions: always-multiline + eol-last: + - error + - always + eqeqeq: + - error + - smart + indent: + - off + #- 4 + #- SwitchCase: 1 + max-len: + - error + - 120 + - ignoreRegExpLiterals: true + ignoreComments: true + # TODO: Disabled for now + #max-statements: + # - error + # - 50 + no-console: error + no-control-regex: 0 + no-useless-call: error + no-useless-computed-key: error + no-useless-concat: error + no-useless-escape: error + no-useless-rename: error + no-useless-return: error + no-empty: + - error + - allowEmptyCatch: true + quote-props: + - error + - as-needed + quotes: + - error + - single + - allowTemplateLiterals: true + avoidEscape: true + semi: + - error + - always + space-before-function-paren: + - error + - anonymous: always + asyncArrow: always + named: never + yoda: + - error + - never + - onlyEquality: true + prefer-const: + - error + - destructuring: all + jsdoc/check-alignment: warn + jsdoc/check-param-names: warn + jsdoc/check-syntax: warn + jsdoc/check-tag-names: off + jsdoc/check-types: warn + jsdoc/empty-tags: warn + jsdoc/require-hyphen-before-param-description: warn + jsdoc/valid-types: warn + sonarjs/no-collection-size-mischeck: error + sonarjs/no-redundant-boolean: error + sonarjs/no-unused-collection: error + sonarjs/prefer-immediate-return: error + sonarjs/prefer-object-literal: error + sonarjs/prefer-single-boolean-return: error + unicorn/filename-case: error +parserOptions: + ecmaVersion: 6 +globals: + define: false + __webpack_public_path__: true diff --git a/shared/.eslintrc.cjs b/shared/.eslintrc.cjs new file mode 100644 index 000000000..0007a026c --- /dev/null +++ b/shared/.eslintrc.cjs @@ -0,0 +1,76 @@ +// Copyright (c) 2023, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +module.exports = { + root: true, + plugins: ['promise', 'requirejs', 'unused-imports'], + extends: ['./.eslint-ce-lib.yml'], + rules: { + 'promise/catch-or-return': 'off', + 'promise/no-new-statics': 'error', + 'promise/no-return-wrap': 'error', + 'promise/param-names': 'error', + 'promise/valid-params': 'error', + }, + overrides: [ + { + files: ['*.ts'], + plugins: ['import', '@typescript-eslint'], + extends: [ + './.eslint-ce-lib.yml', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + ], + env: { + browser: true, + es6: true, + node: false, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + tsconfigRootDir: __dirname, + project: '../tsconfig.json', + }, + rules: { + 'import/no-unresolved': 'off', + 'node/no-missing-imports': 'off', + 'unused-imports/no-unused-imports': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-var-requires': 'error', + '@typescript-eslint/no-explicit-any': 'off', // Too much js code still exists + '@typescript-eslint/ban-ts-comment': 'error', + // TODO: Disabled for now + //'@typescript-eslint/no-unnecessary-condition': 'error', + //'@typescript-eslint/no-unnecessary-type-assertion': 'error', + //'@typescript-eslint/prefer-includes': 'error', + }, + }, + ], +}; diff --git a/lib/common-utils.ts b/shared/common-utils.ts similarity index 91% rename from lib/common-utils.ts rename to shared/common-utils.ts index a306ddabd..9a2481c09 100644 --- a/lib/common-utils.ts +++ b/shared/common-utils.ts @@ -63,3 +63,16 @@ export function basic_comparator(a: T, b: T) { // https://stackoverflow.com/questions/41253310/typescript-retrieve-element-type-information-from-array-type export type ElementType = ArrayType extends readonly (infer T)[] ? T : never; + +const EscapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`', +}; +const EscapeRE = new RegExp(`(?:${Object.keys(EscapeMap).join('|')})`, 'g'); +export function escapeHTML(text: string) { + return text.replace(EscapeRE, str => EscapeMap[str]); +} diff --git a/lib/stacktrace.ts b/shared/stacktrace.ts similarity index 100% rename from lib/stacktrace.ts rename to shared/stacktrace.ts diff --git a/static/ansi-to-html.ts b/static/ansi-to-html.ts index d80197108..31d1e811a 100644 --- a/static/ansi-to-html.ts +++ b/static/ansi-to-html.ts @@ -30,7 +30,7 @@ import _ from 'underscore'; import {AnsiToHtmlOptions, ColorCodes} from './ansi-to-html.interfaces.js'; import {assert, unwrap} from './assert.js'; -import {isString} from '../lib/common-utils.js'; +import {isString} from '../shared/common-utils.js'; const defaults: AnsiToHtmlOptions = { fg: '#FFF', diff --git a/static/assert.ts b/static/assert.ts index 3e52fc9ba..82a2ff50a 100644 --- a/static/assert.ts +++ b/static/assert.ts @@ -22,8 +22,8 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. -import {isString} from '../lib/common-utils.js'; -import {parse} from '../lib/stacktrace.js'; +import {isString} from '../shared/common-utils.js'; +import {parse} from '../shared/stacktrace.js'; // This file defines three assert utilities: // assert(condition, message?, extra_info...?): asserts condition diff --git a/static/main.ts b/static/main.ts index f83933bbe..bb30773f3 100644 --- a/static/main.ts +++ b/static/main.ts @@ -63,7 +63,7 @@ import {Language, LanguageKey} from '../types/languages.interfaces.js'; import {CompilerExplorerOptions} from './global.js'; import {ComponentConfig, EmptyCompilerState, StateWithId, StateWithLanguage} from './components.interfaces.js'; -import * as utils from '../lib/common-utils.js'; +import * as utils from '../shared/common-utils.js'; import {Printerinator} from './print-view.js'; const logos = require.context('../views/resources/logos', false, /\.(png|svg)$/); diff --git a/static/panes/cfg-view.ts b/static/panes/cfg-view.ts index ccb005abc..564194cbc 100644 --- a/static/panes/cfg-view.ts +++ b/static/panes/cfg-view.ts @@ -47,6 +47,7 @@ import TomSelect from 'tom-select'; import {assert, unwrap} from '../assert.js'; import {CompilationResult} from '../compilation/compilation.interfaces.js'; import {CompilerInfo} from '../compiler.interfaces.js'; +import {escapeHTML} from '../../shared/common-utils.js'; const ColorTable = { red: '#FE5D5D', @@ -65,16 +66,6 @@ const MINZOOM = 0.1; const EST_COMPRESSION_RATIO = 0.022; -// https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript -function escapeSVG(text: string) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - function attrs(attributes: Record) { return Object.entries(attributes) .map(([k, v]) => `${k}="${v}"`) @@ -636,7 +627,7 @@ export class Cfg extends Pane { y: block.coordinates.y + top + span_box.height / 2 + parseInt(block_style.paddingTop), class: 'code', fill: span_style.color, - })}>${escapeSVG(text)}`; + })}>${escapeHTML(text)}`; } } doc += ''; diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts index 2de555284..aed007c0d 100644 --- a/static/panes/compiler.ts +++ b/static/panes/compiler.ts @@ -79,6 +79,7 @@ import {CompilerShared} from '../compiler-shared.js'; import {SentryCapture} from '../sentry.js'; import {LLVMIrBackendOptions} from '../compilation/ir.interfaces.js'; import {InstructionSet} from '../instructionsets.js'; +import {escapeHTML} from '../../shared/common-utils.js'; const toolIcons = require.context('../../views/resources/logos', false, /\.(png|svg)$/); @@ -2812,7 +2813,7 @@ export class Compiler extends MonacoPane" + "" + - _.escape(key + '') + + escapeHTML(key + '') + '' + "" + arg.description + @@ -3392,7 +3393,7 @@ export class Compiler extends MonacoPane `
${w}
`).join('\n') + '\n' + (warnings.length > 0 ? infoLine : '') + - _.escape(content || 'No options in use') + + escapeHTML(content || 'No options in use') + `\n
`, html: true, template: @@ -3421,14 +3422,14 @@ export class Compiler extends MonacoPane'); - const versionContent = $('
').html(_.escape(version?.version ?? '')); + const versionContent = $('
').html(escapeHTML(version?.version ?? '')); bodyContent.append(versionContent); if (version?.fullVersion && version.fullVersion.trim() !== version.version.trim()) { const hiddenSection = $('
'); const lines = version.fullVersion .split('\n') .map(line => { - return _.escape(line); + return escapeHTML(line); }) .join('
'); const hiddenVersionText = $('
').html(lines).hide(); @@ -3811,7 +3812,7 @@ export class Compiler extends MonacoPaneFlags: ${_.escape(unwrapString(this.optionsField.val()))}

`; + return `

Flags: ${escapeHTML(unwrapString(this.optionsField.val()))}

`; } override resize() { diff --git a/static/panes/conformance-view.ts b/static/panes/conformance-view.ts index 430b0c49f..a46282cef 100644 --- a/static/panes/conformance-view.ts +++ b/static/panes/conformance-view.ts @@ -43,7 +43,7 @@ import {CompilerInfo} from '../../types/compiler.interfaces.js'; import {CompilationResult} from '../../types/compilation/compilation.interfaces.js'; import {Lib} from '../widgets/libs-widget.interfaces.js'; import {SourceAndFiles} from '../download-service.js'; -import {unique} from '../../lib/common-utils.js'; +import {escapeHTML, unique} from '../../shared/common-utils.js'; import {unwrapString} from '../assert.js'; type ConformanceStatus = { @@ -215,7 +215,7 @@ export class Conformance extends Pane { compilerText = ' ' + this.compilerPickers.length + '/' + this.maxCompilations; } const name = this.paneName ? this.paneName + compilerText : this.getPaneName() + compilerText; - this.container.setTitle(_.escape(name)); + this.container.setTitle(escapeHTML(name)); } addCompilerPicker(config?: AddCompilerPickerConfig): void { diff --git a/static/panes/editor.ts b/static/panes/editor.ts index 476207f65..6d71b6ab4 100644 --- a/static/panes/editor.ts +++ b/static/panes/editor.ts @@ -54,6 +54,7 @@ import {Decoration, Motd} from '../motd.interfaces.js'; import type {escape_html} from 'tom-select/dist/types/utils'; import {Compiler} from './compiler.js'; import {assert, unwrap} from '../assert.js'; +import {escapeHTML, isString} from '../../shared/common-utils.js'; const loadSave = new loadSaveLib.LoadSave(); const languages = options.languages; @@ -1090,7 +1091,7 @@ export class Editor extends MonacoPane { override updateTitle(): void { const name = this.paneName ? this.paneName : this.getPaneName(); - this.container.setTitle(_.escape(name)); + this.container.setTitle(escapeHTML(name)); } updateCompilerName() { @@ -1158,11 +1159,11 @@ export class Executor extends Pane { // `notification` contains HTML from a config file, so is 'safe'. // `version` comes from compiler output, so isn't, and is escaped. const bodyContent = $('
'); - const versionContent = $('
').html(_.escape(version?.version ?? '')); + const versionContent = $('
').html(escapeHTML(version?.version ?? '')); bodyContent.append(versionContent); if (version?.fullVersion) { const hiddenSection = $('
'); - const hiddenVersionText = $('
').html(_.escape(version.fullVersion)).hide(); + const hiddenVersionText = $('`); + this.passesList.append(`
${escapeHTML(pass.name)}
`); } const passDivs = this.passesList.find('.pass'); passDivs.on('click', e => { diff --git a/static/panes/output.ts b/static/panes/output.ts index fa62bb46d..fa8d52a46 100644 --- a/static/panes/output.ts +++ b/static/panes/output.ts @@ -36,6 +36,7 @@ import {OutputState} from './output.interfaces.js'; import {FontScale} from '../widgets/fontscale.js'; import {CompilationResult} from '../../types/compilation/compilation.interfaces.js'; import {CompilerInfo} from '../../types/compiler.interfaces.js'; +import {escapeHTML} from '../../shared/common-utils.js'; function makeAnsiToHtml(color?) { return new AnsiToHtml.Filter({ @@ -338,7 +339,7 @@ export class Output extends Pane { this.eventHub.emit( 'printdata', // eslint-disable-next-line no-useless-concat - `

Output Pane: ${_.escape(this.getPaneName())}

` + `${this.contentRoot.html()}`, + `

Output Pane: ${escapeHTML(this.getPaneName())}

` + `${this.contentRoot.html()}`, ); } } diff --git a/static/panes/pane.ts b/static/panes/pane.ts index 1303e6e7a..72f0dffe0 100644 --- a/static/panes/pane.ts +++ b/static/panes/pane.ts @@ -38,6 +38,7 @@ import {Hub} from '../hub.js'; import {unwrap} from '../assert.js'; import {CompilerInfo} from '../compiler.interfaces.js'; import {CompilationResult} from '../compilation/compilation.interfaces.js'; +import {escapeHTML} from '../../shared/common-utils.js'; /** * Basic container for a tool pane in Compiler Explorer. @@ -237,7 +238,7 @@ export abstract class Pane { /** Update the pane's title, called when the pane name or compiler info changes */ protected updateTitle() { - this.container.setTitle(_.escape(this.getPaneName())); + this.container.setTitle(escapeHTML(this.getPaneName())); } /** Close the pane if the compiler this pane was attached to closes */ @@ -389,7 +390,7 @@ export abstract class MonacoPane extends Pan const extra = this.getExtraPrintData(); this.eventHub.emit( 'printdata', - `

${this.getPrintName()}: ${_.escape(this.getPaneName())}

` + + `

${this.getPrintName()}: ${escapeHTML(this.getPaneName())}

` + (extra ?? '') + `${lines.join('
\n')}
`, ); diff --git a/static/panes/tree.ts b/static/panes/tree.ts index 0a3c346a5..48ba0562b 100644 --- a/static/panes/tree.ts +++ b/static/panes/tree.ts @@ -40,6 +40,7 @@ import {saveAs} from 'file-saver'; import {Container} from 'golden-layout'; import _ from 'underscore'; import {assert, unwrap, unwrapString} from '../assert.js'; +import {escapeHTML} from '../../shared/common-utils.js'; const languages = options.languages; @@ -166,7 +167,7 @@ export class Tree { } private getCustomOutputFilename(): string { - return _.escape(unwrapString(this.customOutputFilenameInput.val())); + return escapeHTML(unwrapString(this.customOutputFilenameInput.val())); } public currentState(): TreeState { @@ -366,7 +367,7 @@ export class Tree { if (file) { this.alertSystem.ask( 'Delete file', - `Are you sure you want to delete ${file.filename ? _.escape(file.filename) : 'this file'}?`, + `Are you sure you want to delete ${file.filename ? escapeHTML(file.filename) : 'this file'}?`, { yes: () => { this.removeFile(fileId); @@ -593,7 +594,7 @@ export class Tree { private async askForOverwriteAndDo(filename): Promise { return new Promise((resolve, reject) => { if (this.multifileService.fileExists(filename)) { - this.alertSystem.ask('Overwrite file', `${_.escape(filename)} already exists, overwrite this file?`, { + this.alertSystem.ask('Overwrite file', `${escapeHTML(filename)} already exists, overwrite this file?`, { yes: () => { this.removeFileByFilename(filename); resolve(); @@ -738,7 +739,7 @@ export class Tree { private updateTitle() { const name = this.paneName ? this.paneName : this.getPaneName(); - this.container.setTitle(_.escape(name)); + this.container.setTitle(escapeHTML(name)); } private close() { diff --git a/static/rison.ts b/static/rison.ts index 755510787..afe797e9a 100644 --- a/static/rison.ts +++ b/static/rison.ts @@ -2,7 +2,7 @@ import {assert, unwrap} from './assert.js'; -import {isString} from '../lib/common-utils.js'; +import {isString} from '../shared/common-utils.js'; ////////////////////////////////////////////////// // diff --git a/static/sentry.ts b/static/sentry.ts index 2fab2b845..760354db8 100644 --- a/static/sentry.ts +++ b/static/sentry.ts @@ -22,7 +22,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. -import {parse} from '../lib/stacktrace.js'; +import {parse} from '../shared/stacktrace.js'; import {options} from './options.js'; diff --git a/static/settings.ts b/static/settings.ts index 9508b2d96..4da6e6197 100644 --- a/static/settings.ts +++ b/static/settings.ts @@ -30,7 +30,7 @@ import {themes, Themes} from './themes.js'; import {AppTheme, ColourScheme, ColourSchemeInfo} from './colour.js'; import {Hub} from './hub.js'; import {EventHub} from './event-hub.js'; -import {keys, isString} from '../lib/common-utils.js'; +import {keys, isString} from '../shared/common-utils.js'; import {assert, unwrapString} from './assert.js'; import {LanguageKey} from '../types/languages.interfaces.js'; diff --git a/static/styles/explorer.scss b/static/styles/explorer.scss index f0800fd99..80b2fb245 100644 --- a/static/styles/explorer.scss +++ b/static/styles/explorer.scss @@ -147,7 +147,7 @@ li.tweet { #printview { display: none; code { - font-family: Consolas, "Liberation Mono", Courier, monospace, Consolas, "Courier New", monospace; + font-family: Consolas, 'Liberation Mono', Courier, monospace, Consolas, 'Courier New', monospace; } .pagebreak { page-break-before: always; @@ -1244,7 +1244,8 @@ html[data-theme='pink'] { } } -#site-templates-list, #site-user-templates-list { +#site-templates-list, +#site-user-templates-list { list-style-type: none; margin: 0; padding: 0; @@ -1258,7 +1259,8 @@ html[data-theme='pink'] { .title { flex: 1 1 auto; } - .title, .delete { + .title, + .delete { background: rgba(0, 0, 0, 0.2); padding: 3px 5px; &:hover { diff --git a/static/themes.ts b/static/themes.ts index d6bd637ed..41228d7fd 100644 --- a/static/themes.ts +++ b/static/themes.ts @@ -26,7 +26,7 @@ import $ from 'jquery'; import {editor} from 'monaco-editor'; import {SiteSettings} from './settings.js'; import GoldenLayout from 'golden-layout'; -import {isString} from '../lib/common-utils.js'; +import {isString} from '../shared/common-utils.js'; export type Themes = 'default' | 'dark' | 'darkplus' | 'pink' | 'system'; diff --git a/static/widgets/compiler-picker-popup.ts b/static/widgets/compiler-picker-popup.ts index 220418023..8fc76c221 100644 --- a/static/widgets/compiler-picker-popup.ts +++ b/static/widgets/compiler-picker-popup.ts @@ -23,12 +23,11 @@ // POSSIBILITY OF SUCH DAMAGE. import $ from 'jquery'; -import _ from 'underscore'; import * as sifter from '@orchidjs/sifter'; import {CompilerInfo} from '../../types/compiler.interfaces'; -import {intersection, remove, unique} from '../../lib/common-utils'; +import {escapeHTML, intersection, remove, unique} from '../../shared/common-utils'; import {unwrap, unwrapString} from '../assert'; import {CompilerPicker} from './compiler-picker'; import {CompilerService} from '../compiler-service'; @@ -86,7 +85,7 @@ export class CompilerPickerPopup { this.architectures.append( ...unique(instruction_sets) .sort() - .map(isa => `${_.escape(isa)}`), + .map(isa => `${escapeHTML(isa)}`), ); // get available compiler types const compilerTypes = compilers.map(compiler => compiler.compilerCategories ?? ['other']).flat(); @@ -94,7 +93,7 @@ export class CompilerPickerPopup { this.compilerTypes.append( ...unique(compilerTypes) .sort() - .map(type => `${_.escape(type)}`), + .map(type => `${escapeHTML(type)}`), ); // search box @@ -179,7 +178,7 @@ export class CompilerPickerPopup { // This is just a good measure to take. If a compiler is ever added that does have special characters in // its name it could interfere with the highlighting (e.g. if your text search is for "<" that won't // highlight). I'm going to defer handling that to a future PR though. - const name = _.escape(compiler.name); + const name = escapeHTML(compiler.name); const compiler_elem = $( `
@@ -212,7 +211,7 @@ export class CompilerPickerPopup { `
-
${_.escape(group.label)}
+
${escapeHTML(group.label)}
`, diff --git a/static/widgets/load-save.ts b/static/widgets/load-save.ts index 3bc341f3e..f32d61b19 100644 --- a/static/widgets/load-save.ts +++ b/static/widgets/load-save.ts @@ -30,6 +30,7 @@ import {ga} from '../analytics.js'; import * as local from '../local.js'; import {Language} from '../../types/languages.interfaces.js'; import {unwrap, unwrapString} from '../assert.js'; +import {escapeHTML} from '../../shared/common-utils.js'; const history = require('../history'); @@ -145,8 +146,8 @@ export class LoadSave { }, delete: () => { this.alertSystem.ask( - `Delete ${_.escape(name)}?`, - `Do you want to delete '${_.escape(name)}'?`, + `Delete ${escapeHTML(name)}?`, + `Do you want to delete '${escapeHTML(name)}'?`, { yes: () => { LoadSave.removeLocalFile(name); @@ -157,8 +158,8 @@ export class LoadSave { }, overwrite: () => { this.alertSystem.ask( - `Overwrite ${_.escape(name)}?`, - `Do you want to overwrite '${_.escape(name)}'?`, + `Overwrite ${escapeHTML(name)}?`, + `Do you want to overwrite '${escapeHTML(name)}'?`, { yes: () => { LoadSave.setLocalFile(name, this.editorText); @@ -244,7 +245,7 @@ export class LoadSave { this.modal?.modal('hide'); this.alertSystem.ask( 'Replace current?', - `Do you want to replace the existing saved file '${_.escape(name)}'?`, + `Do you want to replace the existing saved file '${escapeHTML(name)}'?`, {yes: doneCallback}, ); } else { diff --git a/static/widgets/site-templates-widget.ts b/static/widgets/site-templates-widget.ts index ab8c0b034..b579d6b0f 100644 --- a/static/widgets/site-templates-widget.ts +++ b/static/widgets/site-templates-widget.ts @@ -23,7 +23,6 @@ // POSSIBILITY OF SUCH DAMAGE. import $ from 'jquery'; -import _ from 'underscore'; import {SiteTemplatesType, UserSiteTemplate} from '../../types/features/site-templates.interfaces.js'; import {assert, unwrap, unwrapString} from '../assert.js'; @@ -32,6 +31,7 @@ import * as local from '../local.js'; import * as url from '../url.js'; import GoldenLayout from 'golden-layout'; import {Alert} from './alert.js'; +import {escapeHTML} from '../../shared/common-utils.js'; class SiteTemplatesWidget { private readonly modal: JQuery; @@ -112,7 +112,7 @@ class SiteTemplatesWidget { } else { for (const [id, {title, data}] of Object.entries(userTemplates)) { const li = $(`
  • `); - $(`
    ${_.escape(title)}
    `) + $(`
    ${escapeHTML(title)}
    `) .attr('data-data', data) .appendTo(li); $(`
    `).appendTo(li); @@ -136,7 +136,7 @@ class SiteTemplatesWidget { // Note: Trusting the server-provided data attribute siteTemplatesList.append( `
  • ` + - `
    ${_.escape( + `
    ${escapeHTML( name, )}
    ` + `
  • `, diff --git a/static/widgets/timing-info-widget.ts b/static/widgets/timing-info-widget.ts index aa650803b..1458744c9 100644 --- a/static/widgets/timing-info-widget.ts +++ b/static/widgets/timing-info-widget.ts @@ -27,8 +27,8 @@ import {Settings} from '../settings.js'; import {Chart, ChartData, defaults} from 'chart.js'; import 'chart.js/auto'; import {CompilationResult} from '../../types/compilation/compilation.interfaces.js'; -import _ from 'underscore'; import {unwrap} from '../assert.js'; +import {isString} from '../../shared/common-utils.js'; type Data = ChartData<'bar', number[], string> & {steps: number}; @@ -116,7 +116,7 @@ function initializeChartDataFromResult(compileResult: CompilationResult, totalTi pushTimingInfo(data, 'Process execution result', compileResult.processExecutionResultTime); } - if (compileResult.hasLLVMOptPipelineOutput && !_.isString(compileResult.llvmOptPipelineOutput)) { + if (compileResult.hasLLVMOptPipelineOutput && !isString(compileResult.llvmOptPipelineOutput)) { if (compileResult.llvmOptPipelineOutput?.clangTime !== undefined) { pushTimingInfo(data, 'Llvm opt pipeline clang time', compileResult.llvmOptPipelineOutput.clangTime); } diff --git a/test/common-utils-tests.ts b/test/common-utils-tests.ts new file mode 100644 index 000000000..0ade79f27 --- /dev/null +++ b/test/common-utils-tests.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2023, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +import {escapeHTML} from '../shared/common-utils.js'; + +describe('HTML Escape Test Cases', () => { + it('should prevent basic injection', () => { + escapeHTML("").should.equal(`<script>alert('hi');</script>`); + }); + it('should prevent tag injection', () => { + escapeHTML('\'"`>').should.equal(`'"`>`); + }); +});