Common utilities and type work (#5200)

This PR refactors some common utilities out of lib/ and into shared/ and
eliminates some use of underscore.js, as well as general type
improvements done along the way.
This commit is contained in:
Jeremy Rifkin
2023-06-28 19:13:10 -05:00
committed by GitHub
parent 55cd371404
commit 4260313e13
45 changed files with 553 additions and 256 deletions

63
app.ts
View File

@@ -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<string>('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('=======================================');
// silly express typing, passing undefined is fine but
if (defArgs.hostname) {
server.listen(_port, defArgs.hostname);
} else {
server.listen(_port);
}
}
}

View File

@@ -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

View File

@@ -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://';

View File

@@ -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<string, OptionsHandlerLibrary>, 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<string, VersionInfo>;
copiedLibraries[libid] = libcopy;
});
@@ -304,7 +307,7 @@ export class BaseCompiler implements ICompiler {
return copiedLibraries;
}
getSupportedLibraries(supportedLibrariesArr, allLibs) {
getSupportedLibraries(supportedLibrariesArr: string[], allLibs: Record<string, OptionsHandlerLibrary>) {
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,13 +767,14 @@ export class BaseCompiler implements ICompiler {
return false;
}
});
}),
),
})
.flat(3),
);
const sortedlinks: string[] = [];
_.each(links, libToInsertName => {
for (const libToInsertName of links) {
if (libToInsertName) {
const libToInsertObj = dictionary[libToInsertName];
let idxToInsert = sortedlinks.length;
@@ -825,45 +829,43 @@ export class BaseCompiler implements ICompiler {
} 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<string, any>,
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<string, Library>;
}
async getTargetsAsOverrideValues(): Promise<CompilerOverrideOption[]> {
@@ -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) {

View File

@@ -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<string, Language>;
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);

View File

@@ -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 {

View File

@@ -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<string, any> | null = null;
private clientOptions: ClientOptionsType | null = null;
private readonly compileCounter: Counter<string> = new PromClient.Counter({
name: 'ce_compilations_total',
help: 'Number of compilations',
@@ -245,7 +246,7 @@ export class CompileHandler {
}
}
async setCompilers(compilers: PreliminaryCompilerInfo[], clientOptions: Record<string, any>) {
async setCompilers(compilers: PreliminaryCompilerInfo[], clientOptions: ClientOptionsType) {
// Be careful not to update this.compilersById until we can replace it entirely.
const compilersById = {};
try {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,7 +683,8 @@ const definitions: Record<LanguageKey, LanguageDefinition> = {
},
};
export const languages: Record<LanguageKey, Language> = _.mapObject(definitions, (lang, key) => {
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');
@@ -698,5 +698,6 @@ export const languages: Record<LanguageKey, Language> = _.mapObject(definitions,
supportsExecute: false,
example,
};
return def;
});
return [key, def];
}),
) as Record<LanguageKey, Language>;

View File

@@ -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,

View File

@@ -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));
});
// silly express typing, passing undefined is fine but
if (hostname) {
metricsServer.listen(serverPort, hostname);
} else {
metricsServer.listen(serverPort);
}
}

View File

@@ -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<string, VersionInfo>;
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<any, any>;
libs: Record<string, Record<string, OptionsHandlerLibrary>>;
remoteLibs: Record<any, any>;
tools: Record<any, any>;
defaultLibs: Record<LanguageKey, string>;
@@ -119,7 +132,7 @@ export class ClientOptionsHandler {
supportsLibraryCodeFilterPerLanguage: Record<LanguageKey, boolean>;
supportsLibraryCodeFilter: boolean;
remoteLibs: Record<any, any>;
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<string, string>) {
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<string, VersionInfo>;
examples: string[];
options: string[];
packagedheaders?: boolean;
};
// Record language -> {Record lib name -> lib}
const libraries: Record<string, Record<string, Library>> = {};
const libraries: Record<string, Record<string, OptionsHandlerLibrary>> = {};
for (const [lang, forLang] of Object.entries(baseLibs)) {
if (lang && forLang) {
libraries[lang] = {};

View File

@@ -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';

View File

@@ -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<string, Record<string, PropertyValue>> = {};
@@ -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 {

View File

@@ -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';

7
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

94
shared/.eslint-ce-lib.yml Normal file
View File

@@ -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

76
shared/.eslintrc.cjs Normal file
View File

@@ -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',
},
},
],
};

View File

@@ -63,3 +63,16 @@ export function basic_comparator<T>(a: T, b: T) {
// https://stackoverflow.com/questions/41253310/typescript-retrieve-element-type-information-from-array-type
export type ElementType<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer T)[] ? T : never;
const EscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
};
const EscapeRE = new RegExp(`(?:${Object.keys(EscapeMap).join('|')})`, 'g');
export function escapeHTML(text: string) {
return text.replace(EscapeRE, str => EscapeMap[str]);
}

View File

@@ -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',

View File

@@ -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

View File

@@ -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)$/);

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function attrs(attributes: Record<string, string | number | null>) {
return Object.entries(attributes)
.map(([k, v]) => `${k}="${v}"`)
@@ -636,7 +627,7 @@ export class Cfg extends Pane<CfgState> {
y: block.coordinates.y + top + span_box.height / 2 + parseInt(block_style.paddingTop),
class: 'code',
fill: span_style.color,
})}>${escapeSVG(text)}</text>`;
})}>${escapeHTML(text)}</text>`;
}
}
doc += '</svg>';

View File

@@ -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<monaco.editor.IStandaloneCodeEditor, Co
argumentButton.html(
"<div class='argmenuitem'>" +
"<span class='argtitle'>" +
_.escape(key + '') +
escapeHTML(key + '') +
'</span>' +
"<span class='argdescription'>" +
arg.description +
@@ -3392,7 +3393,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
warnings.map(w => `<div class="compiler-arg-warning">${w}</div>`).join('\n') +
'\n' +
(warnings.length > 0 ? infoLine : '') +
_.escape(content || 'No options in use') +
escapeHTML(content || 'No options in use') +
`\n<div class="compiler-arg-warning-shake-setting"></div>`,
html: true,
template:
@@ -3421,14 +3422,14 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
// `notification` contains HTML from a config file, so is 'safe'.
// `version` comes from compiler output, so isn't, and is escaped.
const bodyContent = $('<div>');
const versionContent = $('<div>').html(_.escape(version?.version ?? ''));
const versionContent = $('<div>').html(escapeHTML(version?.version ?? ''));
bodyContent.append(versionContent);
if (version?.fullVersion && version.fullVersion.trim() !== version.version.trim()) {
const hiddenSection = $('<div>');
const lines = version.fullVersion
.split('\n')
.map(line => {
return _.escape(line);
return escapeHTML(line);
})
.join('<br/>');
const hiddenVersionText = $('<div>').html(lines).hide();
@@ -3811,7 +3812,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
}
override getExtraPrintData() {
return `<p>Flags: <code>${_.escape(unwrapString(this.optionsField.val()))}</code></p>`;
return `<p>Flags: <code>${escapeHTML(unwrapString(this.optionsField.val()))}</code></p>`;
}
override resize() {

View File

@@ -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<ConformanceViewState> {
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 {

View File

@@ -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<monaco.editor.IStandaloneCodeEditor, Edit
}
}
// navigator.language[s] is supposed to return strings, but hey, you never know
if (lang !== result && _.isString(lang)) {
if (lang !== result && isString(lang)) {
const primaryLanguageSubtagIdx = lang.indexOf('-');
result = lang.substring(0, primaryLanguageSubtagIdx).toLowerCase();
}
@@ -1913,7 +1914,7 @@ export class Editor extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Edit
if (name.endsWith('CMakeLists.txt')) {
this.changeLanguage('cmake');
}
this.container.setTitle(_.escape(customName));
this.container.setTitle(escapeHTML(customName));
}
// Called every time we change language, so we get the relevant code

View File

@@ -59,6 +59,7 @@ import {SourceAndFiles} from '../download-service.js';
import {ICompilerShared} from '../compiler-shared.interfaces.js';
import {CompilerShared} from '../compiler-shared.js';
import {LangInfo} from './compiler-request.interfaces.js';
import {escapeHTML} from '../../shared/common-utils.js';
const languages = options.languages;
@@ -1122,7 +1123,7 @@ export class Executor extends Pane<ExecutorState> {
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<ExecutorState> {
// `notification` contains HTML from a config file, so is 'safe'.
// `version` comes from compiler output, so isn't, and is escaped.
const bodyContent = $('<div>');
const versionContent = $('<div>').html(_.escape(version?.version ?? ''));
const versionContent = $('<div>').html(escapeHTML(version?.version ?? ''));
bodyContent.append(versionContent);
if (version?.fullVersion) {
const hiddenSection = $('<div>');
const hiddenVersionText = $('<div>').html(_.escape(version.fullVersion)).hide();
const hiddenVersionText = $('<div>').html(escapeHTML(version.fullVersion)).hide();
const clickToExpandContent = $('<a>')
.attr('href', 'javascript:;')
.text('Toggle full version output')

View File

@@ -47,6 +47,7 @@ import {
import {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 MIN_SIDEBAR_WIDTH = 100;
@@ -320,7 +321,7 @@ export class LLVMOptPipeline extends MonacoPane<monaco.editor.IStandaloneDiffEdi
className += ' firstMachinePass';
isFirstMachinePass = false;
}
this.passesList.append(`<div data-i="${i}" class="pass ${className}">${_.escape(pass.name)}</div>`);
this.passesList.append(`<div data-i="${i}" class="pass ${className}">${escapeHTML(pass.name)}</div>`);
}
const passDivs = this.passesList.find('.pass');
passDivs.on('click', e => {

View File

@@ -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<OutputState> {
this.eventHub.emit(
'printdata',
// eslint-disable-next-line no-useless-concat
`<h1>Output Pane: ${_.escape(this.getPaneName())}</h1>` + `<code>${this.contentRoot.html()}</code>`,
`<h1>Output Pane: ${escapeHTML(this.getPaneName())}</h1>` + `<code>${this.contentRoot.html()}</code>`,
);
}
}

View File

@@ -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<S> {
/** 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<E extends monaco.editor.IEditor, S> extends Pan
const extra = this.getExtraPrintData();
this.eventHub.emit(
'printdata',
`<h1>${this.getPrintName()}: ${_.escape(this.getPaneName())}</h1>` +
`<h1>${this.getPrintName()}: ${escapeHTML(this.getPaneName())}</h1>` +
(extra ?? '') +
`<code>${lines.join('<br/>\n')}</code>`,
);

View File

@@ -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<void> {
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() {

View File

@@ -2,7 +2,7 @@
import {assert, unwrap} from './assert.js';
import {isString} from '../lib/common-utils.js';
import {isString} from '../shared/common-utils.js';
//////////////////////////////////////////////////
//

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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 => `<span class="architecture" data-value=${_.escape(isa)}>${_.escape(isa)}</span>`),
.map(isa => `<span class="architecture" data-value=${escapeHTML(isa)}>${escapeHTML(isa)}</span>`),
);
// 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 => `<span class="compiler-type" data-value=${_.escape(type)}>${_.escape(type)}</span>`),
.map(type => `<span class="compiler-type" data-value=${escapeHTML(type)}>${escapeHTML(type)}</span>`),
);
// 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 = $(
`
<div class="compiler d-flex" data-value="${compiler.id}">
@@ -212,7 +211,7 @@ export class CompilerPickerPopup {
`
<div class="group-wrapper">
<div class="group">
<div class="label">${_.escape(group.label)}</div>
<div class="label">${escapeHTML(group.label)}</div>
</div>
</div>
`,

View File

@@ -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 {

View File

@@ -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 = $(`<li></li>`);
$(`<div class="title">${_.escape(title)}</div>`)
$(`<div class="title">${escapeHTML(title)}</div>`)
.attr('data-data', data)
.appendTo(li);
$(`<div class="delete" data-id="${id}"><i class="fa-solid fa-trash"></i></div>`).appendTo(li);
@@ -136,7 +136,7 @@ class SiteTemplatesWidget {
// Note: Trusting the server-provided data attribute
siteTemplatesList.append(
`<li>` +
`<div class="title" data-data="${data}" data-name="${name.replace(/[^a-z]/gi, '')}">${_.escape(
`<div class="title" data-data="${data}" data-name="${name.replace(/[^a-z]/gi, '')}">${escapeHTML(
name,
)}</div>` +
`</li>`,

View File

@@ -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);
}

View File

@@ -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("<script>alert('hi');</script>").should.equal(`&lt;script&gt;alert(&#x27;hi&#x27;);&lt;/script&gt;`);
});
it('should prevent tag injection', () => {
escapeHTML('\'"`>').should.equal(`&#x27;&quot;&#x60;&gt;`);
});
});