tio.js
Version:
Evaluate code in a sandboxed environment everywhere with TryItOnline.
256 lines • 10.5 kB
JavaScript
/**
* @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