UNPKG

tio.js

Version:

Evaluate code in a sandboxed environment everywhere with TryItOnline.

256 lines 10.5 kB
/** * @name tio.js * @description Evaluate code in a sandboxed environment everywhere with TryItOnline. * @copyright Copyright (c) 2021-2025 null8626 * @license MIT * @author null8626 * @version 4.1.0 */ import { deflateRaw, gunzip } from 'node:zlib'; import { inspect, promisify } from 'node:util'; import { randomBytes } from 'node:crypto'; import { validStringArray, request, Timeout } from './util.js'; import { TioError } from './error.js'; import languages from './languages.js'; const SCRIPT_REGEX = /<script src="(\/static\/[0-9a-f]+-frontend\.js)" defer><\/script>/; const RUNURL_REGEX = /^var runURL = "\/cgi-bin\/static\/([^"]+)";$/m; const DEBUG_REGEX = /([\s\S]*)Real time: ([\d.]+) s\nUser time: ([\d.]+) s\nSys\. time: ([\d.]+) s\nCPU share: ([\d.]+) %\nExit code: (\d+)$/; const deflateRawAsync = promisify(deflateRaw); const gunzipAsync = promisify(gunzip); let runURL = null; let nextRefresh = 0; let defaultLanguage = 'javascript-node'; let defaultTimeout = Infinity; let defaultCflags = []; let defaultArgv = []; let refreshTimeout = 0; async function prepare() { if (runURL !== null && Date.now() < nextRefresh) { return; } const scrapeResponse = await (await request('/')).text(); const frontendJSURL = scrapeResponse.match(SCRIPT_REGEX)?.[1]; /* node:coverage ignore next 5 */ if (frontendJSURL === undefined) { throw new TioError('An error occurred while scraping tio.run. Please try again later or report this bug to the developer.'); } const frontendJSResponse = await request(frontendJSURL); refreshTimeout = Number(frontendJSResponse.headers .get('Cache-Control') ?.match(/max-age=(\d+)/)?.[1]) * 1000; /* node:coverage ignore next 5 */ if (!refreshTimeout || isNaN(refreshTimeout)) { throw new TioError('An error occurred while scraping tio.run. Please try again later or report this bug to the developer.'); } const frontendJS = await frontendJSResponse.text(); const newRunURL = frontendJS.match(RUNURL_REGEX)?.[1]; /* node:coverage ignore next 5 */ if (newRunURL === undefined) { throw new TioError('An error occurred while scraping tio.run. Please try again later or report this bug to the developer.'); } runURL = newRunURL; nextRefresh = Date.now() + refreshTimeout; } async function evaluate(code, options, ab, tm) { const cflags = options.cflags.map(f => `${f}\0`).join(''); const argv = options.argv.map(a => `${a}\0`).join(''); try { const response = await request(`/cgi-bin/static/${runURL}/${randomBytes(16).toString('hex')}`, { method: 'POST', body: new Uint8Array(await deflateRawAsync(`Vargs\0${options.argv.length}\0${argv}Vlang\0\x31\0${options.language}\0VTIO_CFLAGS\0${options.cflags.length}\0${cflags}VTIO_OPTIONS\0\x30\0F.code.tio\0${code.length}\0${code}F.input.tio\0\x30\0R`, { level: 9 })), signal: ab.signal }); const reader = response.body?.getReader(); const chunks = []; let total = 0; if (reader) { let data; while ((data = await Promise.race([reader.read(), tm.promise])) !== null) { if (data.done) { const rawResult = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { rawResult.set(chunk, offset); offset += chunk.length; } return (await gunzipAsync(rawResult)).toString(); } chunks.push(data.value); total += data.value.length; } void reader.cancel(); } /* node:coverage ignore next 5 */ } catch (err) { if (err.name !== 'AbortError') { throw err; } } return null; } /** * The optional options to be passed onto tio(). * @typedef {object} TioOptions * @property {string} [language] - The preferred language ID. * @property {number} [timeout] - How much milliseconds for the client to wait before the request times out. Great for surpressing infinite loops. Defaults to Infinity. * @property {string[]} [cflags] - Extra arguments to be passed onto the compiler (Only works in compiled languages). * @property {string[]} [argv] - Custom command-line arguments. * @public */ /** * The evaluated response. * @typedef {object} TioResponse * @property {string} output - The output from the command line. * @property {boolean} timedOut - Whether the request timed out or not. * @property {number} realTime - The time it takes to evaluate the code in real-time. * @property {number} userTime - The time it takes to evaluate the code in user-time. * @property {number} sysTime - The time it takes to evaluate the code in system-time. * @property {number} CPUshare - The CPU share percentage value. * @property {number} exitCode - The program's exit code. * @public */ /** * Evaluates a code. * @param {string} code - The source code to be evaluated. * @param {TioOptions} [options] - The optional options to be passed in to override the default options. * @returns {Promise<TioResponse>} The evaluated response. * @async * @throws {TioError} The user supplied invalid arguments or the client couldn't scrape tio.run. * @throws {TioHttpError} The client received an invalid HTTP response from the tio.run servers. This is usually not expected. * @public * @see {@link https://github.com/null8626/tio.js#examples} * @example await tio('console.log("Hello, World!");') * @example await tio('print("Hello, World!")', { language: 'python3' }) * @example await tio('console.log("Hello, World!");', { timeout: 2000 }) * @example await tio('console.log(process.argv.slice(2).join(", "));', { argv: ['Hello', 'World!'] }) */ const tio = (async (code, options) => { options ??= {}; if ('timeout' in options && options.timeout !== Infinity && (!Number.isSafeInteger(options.timeout) || options.timeout < 500)) { throw new TioError(`Timeout must be a valid integer and it's value must be 500 or greater. Got ${inspect(options.timeout)}`); } else if ('language' in options && options.language !== defaultLanguage && !languages.includes(options.language)) { throw new TioError(`Unsupported/invalid language ID provided (Got ${inspect(options.language)}), a list of supported language IDs can be seen in \`tio.languages\`.`); } else if ('cflags' in options && !validStringArray(options.cflags)) { throw new TioError(`Compiler flags must be a valid array of strings. Got ${inspect(options.cflags)}`); } else if ('argv' in options && !validStringArray(options.argv)) { throw new TioError(`Command-line arguments must be a valid array of strings. Got ${inspect(options.argv)}`); } options = Object.assign({ language: defaultLanguage, timeout: defaultTimeout, cflags: defaultCflags, argv: defaultArgv }, options); await prepare(); const ab = new AbortController(); const result = await evaluate(code, options, ab, new Timeout(ab, options.timeout)); ab.abort(); if (result === null) { const timeoutInSecs = options.timeout / 1000; return Object.freeze({ output: `Request timed out after ${options.timeout}ms`, timedOut: true, realTime: timeoutInSecs, userTime: timeoutInSecs, sysTime: timeoutInSecs, CPUshare: 0, exitCode: 124 }); } const s = result.substring(16).split(result.substring(0, 16)); const [debug, realTime, userTime, sysTime, CPUshare, exitCode] = s[1] .match(DEBUG_REGEX) .slice(1); return Object.freeze({ output: s[0] || debug, timedOut: false, realTime: parseFloat(realTime), userTime: parseFloat(userTime), sysTime: parseFloat(sysTime), CPUshare: parseFloat(CPUshare), exitCode: Number(exitCode) }); }); Object.defineProperties(tio, { languages: { configurable: false, enumerable: true, writable: false, value: languages }, defaultLanguage: { configurable: false, enumerable: true, get() { return defaultLanguage; }, set(lang) { if (lang !== defaultLanguage && !languages.includes(lang)) { throw new TioError(`Unsupported/invalid language ID provided (${lang}), a list of supported language IDs can be seen in \`tio.languages\`.`); } defaultLanguage = lang; } }, defaultTimeout: { configurable: false, enumerable: true, get() { return defaultTimeout; }, set(timeout) { if (timeout !== Infinity && (!Number.isSafeInteger(timeout) || timeout < 500)) { throw new TioError(`Timeout must be a valid integer and it's value must be 500 or greater. Got ${inspect(timeout)}`); } defaultTimeout = timeout; } }, defaultCflags: { configurable: false, enumerable: true, get() { return defaultCflags; }, set(cflags) { if (!validStringArray(cflags)) { throw new TioError(`Compiler flags must be a valid array of strings. Got ${inspect(cflags)}`); } defaultCflags = cflags; } }, defaultArgv: { configurable: false, enumerable: true, get() { return defaultArgv; }, set(argv) { if (!validStringArray(argv)) { throw new TioError(`Command-line arguments must be a valid array of strings. Got ${inspect(argv)}`); } defaultArgv = argv; } }, /* node:coverage ignore next 18 */ refreshTimeout: { configurable: false, enumerable: true, get() { console.warn("[DEPRECATED] refreshTimeout is deprecated. tio.js now only depends on tio.run's Cache-Control response header."); return refreshTimeout; }, set(_timeout) { console.warn("[DEPRECATED] refreshTimeout is deprecated. tio.js now only depends on tio.run's Cache-Control response header."); } } }); export default tio; //# sourceMappingURL=index.js.map