Skip to content

Verifier fixes #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 3, 2024
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,3 @@ results

npm-debug.log
node_modules/
/.vscode/*
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": []
}
9 changes: 0 additions & 9 deletions src/challenges/c2-sort-the-numbers/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import type { Challenge } from '../types.js';

class ArrayNoSort extends Array {
public override sort() {
return ['allowed', 'no', 'sort'] as unknown as this;
}
}

const challenge: Challenge<[arr: readonly number[]], readonly number[]> = {
title: 'Sort The Numbers',
description:
Expand All @@ -20,9 +14,6 @@ const challenge: Challenge<[arr: readonly number[]], readonly number[]> = {
output: [1, 1, 2, 10, 12, 16, 22, 23, 33, 34, 65, 150, 250, 300],
},
],
context: {
Array: ArrayNoSort,
},
};

export default challenge;
2 changes: 1 addition & 1 deletion src/challenges/c5-add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const challenge: Challenge<[input: [number, number]], number> = {
},
],
context: {
eval: () => 42,
eval: () => "eval is not the answer you're looking for",
},
assertRules: (playString) => {
if (playString.includes('+')) {
Expand Down
1 change: 1 addition & 0 deletions src/challenges/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ export interface Challenge<
assertRules?: (playString: string) => void;
context?: Record<string, unknown>;
timeLimitMinutes?: number;
runBefore?: () => void;
}
11 changes: 2 additions & 9 deletions src/game/game-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import * as url from 'url';

import type { VerifyJob } from './verifier.js';
import * as game from './game.js';
import { challenges } from '../challenges/index.js';
import { Primitive } from '../challenges/types.js';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

Expand All @@ -17,15 +15,10 @@ export const verify = (
) => {
const currentGame = game.getOrError();
const verifier = child_process.fork(path.resolve(__dirname, 'verifier.ts'));
const challenge = challenges.find((ch) => ch.key === currentGame.key);

if (!challenge) {
throw new Error('Challenge not found');
}

const job: VerifyJob<readonly Primitive[], Primitive> = {
const job: VerifyJob = {
file,
challenge,
key: currentGame.key,
};

verifier.send(job);
Expand Down
26 changes: 7 additions & 19 deletions src/game/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,21 @@ export const formatValue = (value: unknown): string => {
return `Unknown value: ${value}`;
};

export const formatTypeAndValue = (value: unknown, actual: Primitive) => {
export const formatTypeAndValue = (
value: Primitive | readonly unknown[] | Record<string, unknown>,
isResult: Primitive
) => {
if (value === null) {
return 'null';
}

if (typeof value === 'undefined') {
return 'undefined';
}

if (typeof value === 'function') {
return 'function';
return `${isResult ? 'different ' : ''}function`;
}

if (Array.isArray(value)) {
return (actual ? 'different ' : '') + 'array of ' + value.length + ' items';
}

if (typeof value === 'string') {
return (
(actual ? 'different ' : '') + 'string of ' + value.length + ' chars'
);
}

if (typeof value === 'object') {
return (actual ? 'different ' : '') + 'object';
return `array ${formatValue(value)}`;
}

const digits = value.toString().replace(/[^0-9]/g, '').length;
return (actual ? 'different ' : '') + `${digits} digit number`;
return `${typeof value} ${formatValue(value)}`;
};
50 changes: 31 additions & 19 deletions src/game/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,35 @@ import lodash from 'lodash';
import { readFileSync } from 'fs';
import { runInNewContext } from 'vm';

import type { Challenge, Primitive } from '../challenges/types.js';
import { formatTypeAndValue } from './utils.js';
import { formatTypeAndValue, formatValue } from './utils.js';
import { challenges } from '../challenges/index.js';
import { Primitive } from '../challenges/types.js';

export interface VerifyJob<
A extends readonly Primitive[],
R extends Primitive,
> {
export interface VerifyJob {
file: string;
challenge: Omit<Challenge<A, R>, 'example'> & {
key: string;
solution: string;
example: string;
};
key: string;
}

const hasKey = <K extends string>(
obj: object,
key: K
): obj is { [P in K]: unknown } => key in obj;

process.on('message', (entry: VerifyJob<readonly Primitive[], Primitive>) => {
process.on('message', (entry: VerifyJob) => {
try {
const challenge = challenges.find((ch) => ch.key === entry.key);

if (!challenge) {
throw new Error('Challenge not found');
}

const script = readFileSync(entry.file, 'utf8');
const context: { play?: unknown; module: { exports: unknown } } = {
...challenge.context,
module: {
exports: {},
exports: {
runBefore: challenge.runBefore,
},
},
};

Expand All @@ -38,6 +41,15 @@ process.on('message', (entry: VerifyJob<readonly Primitive[], Primitive>) => {

runInNewContext(toRun, context);

if (
typeof context.module.exports === 'object' &&
!!context.module.exports &&
hasKey(context.module.exports, 'runBefore') &&
typeof context.module.exports.runBefore === 'function'
) {
context.module.exports.runBefore();
}

const play =
typeof context.module.exports === 'function'
? context.module.exports
Expand All @@ -56,7 +68,9 @@ process.on('message', (entry: VerifyJob<readonly Primitive[], Primitive>) => {
}

try {
entry.challenge.assertions.forEach((assertion) => {
challenge.assertRules?.(script);

challenge.assertions.forEach((assertion) => {
const result = play(...assertion.input);

const expected =
Expand All @@ -75,20 +89,18 @@ process.on('message', (entry: VerifyJob<readonly Primitive[], Primitive>) => {
)} but received ${formatTypeAndValue(
result,
true
)} when supplied with ${formatTypeAndValue(assertion.input, false)}`
)} when supplied with arguments: ${assertion.input.map((input) => formatValue(input)).join(', ')}`
);
}
});

entry.challenge.assertRules?.(entry.challenge.solution);
} catch (err) {
return process.send?.({
err:
typeof err === 'string'
? err
: err instanceof Error
? err.message
: 'Unknown error',
: `Unknown error ${err}`,
valid: false,
});
}
Expand All @@ -99,7 +111,7 @@ process.on('message', (entry: VerifyJob<readonly Primitive[], Primitive>) => {
console.error('Verifier failed with error:', err);

process.send?.({
err: 'Your script contains an error',
err: `Your script contains an error: ${err}`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exposing the error allows exfiltrating the test data....

valid: false,
});
}
Expand Down
Loading