push
This commit is contained in:
Executable
+7
@@ -0,0 +1,7 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||
spec: "@yarnpkg/plugin-typescript"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-berry.cjs
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
FROM node:lts
|
||||
|
||||
RUN apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2
|
||||
|
||||
WORKDIR /problem
|
||||
ADD .yarnrc.yml .
|
||||
ADD .yarn ./.yarn/
|
||||
ADD package.json .
|
||||
ADD yarn.lock .
|
||||
|
||||
RUN yarn
|
||||
|
||||
ADD . .
|
||||
RUN yarn build
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
<link rel="stylesheet" href="/css/calc.css">
|
||||
<script id="program" language="json" type="{{ content-type }}">
|
||||
{{ program }}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
window.addEventListener("load", () => {
|
||||
import("/js/calc.mjs");
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="body">
|
||||
<h1 class="title" id="name">Loading</h1>
|
||||
<div id="input"></div>
|
||||
<span id="output"></span>
|
||||
<span id="error"></span>
|
||||
<button id="report">Show me your math!</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
.body #input {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.body #input > input {
|
||||
justify-self: center;
|
||||
outline: none;
|
||||
padding: 16px;
|
||||
margin: 16px;
|
||||
font-size: 24px;
|
||||
border: 2px solid #2a2a2a;
|
||||
width: 128px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body #output {
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body #output:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body #output::before {
|
||||
content: "Result:";
|
||||
color: #2a2a2a;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.body #error {
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
color: #e21c1c;
|
||||
}
|
||||
|
||||
.body #error:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body #error::before {
|
||||
content: "Error:";
|
||||
color: #e94e4e;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
body {
|
||||
font-family: 'Oxygen';
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.body > * {
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
.body .title {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
.body #name {
|
||||
max-width: 400px;
|
||||
font-size: 24px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.body #program {
|
||||
resize: vertical;
|
||||
}
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<script type="module">
|
||||
window.addEventListener("load", () => {
|
||||
import("/js/main.mjs");
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="body">
|
||||
<h1 class="title">Upload your Program to Run</h1>
|
||||
<input id="name" placeholder="New Program"/>
|
||||
<textarea id="program" style="min-height: 200px;">
|
||||
(-b + sqrt(b^2 - 4a*c)) / 2a
|
||||
</textarea>
|
||||
<div>
|
||||
Choose a program type:
|
||||
<select id="content-type">
|
||||
<option value="application/x-yaca-ast">AST</option>
|
||||
<option value="application/x-yaca-code" selected>Code</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="upload">Upload</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
const allowedMathFunctions = new Set([
|
||||
"abs",
|
||||
"acos",
|
||||
"asin",
|
||||
"atan",
|
||||
"cos",
|
||||
"sin",
|
||||
"tan",
|
||||
"ceil",
|
||||
"floor",
|
||||
"exp",
|
||||
"log",
|
||||
"log2",
|
||||
"log10",
|
||||
"sqrt",
|
||||
]);
|
||||
|
||||
export default function astToJs(ast) {
|
||||
if (typeof ast !== "object" || ast === null) {
|
||||
throw new Error("Ast node must be an object");
|
||||
}
|
||||
|
||||
switch (ast.kind) {
|
||||
case "number": {
|
||||
if (typeof ast.value !== "number") {
|
||||
throw new Error("Number is of the wrong type");
|
||||
}
|
||||
|
||||
return {
|
||||
code: `${ast.value}`,
|
||||
variables: new Set(),
|
||||
};
|
||||
}
|
||||
case "variable": {
|
||||
if (typeof ast.variable !== "string") {
|
||||
throw new Error("Variable name not specified");
|
||||
}
|
||||
|
||||
if (!ast.variable.match(/^[a-z][a-z0-9_]*$/)) {
|
||||
throw new Error(`Invalid variable name: ${ast.variable}`);
|
||||
}
|
||||
|
||||
const name = `var_${ast.variable}`;
|
||||
|
||||
return {
|
||||
code: name,
|
||||
variables: new Set([name]),
|
||||
}
|
||||
}
|
||||
case "function": {
|
||||
const { name, argument } = ast;
|
||||
const { code: argumentCode, variables } = astToJs(argument);
|
||||
|
||||
if (typeof name !== "string") {
|
||||
throw new Error("Function name must be a string");
|
||||
}
|
||||
|
||||
if (!allowedMathFunctions.has(name)) {
|
||||
throw new Error(`Invalid function: ${name}`);
|
||||
}
|
||||
|
||||
const code = `Math.${name}(${argumentCode})`;
|
||||
|
||||
return { code, variables };
|
||||
}
|
||||
case "unop": {
|
||||
const { code: nestedCode, variables } = astToJs(ast.value);
|
||||
const op =
|
||||
ast.op === "negate" ? "-" :
|
||||
ast.op === "invert" ? "~" :
|
||||
null;
|
||||
|
||||
if (op === null) {
|
||||
throw new Error("Invalid unary operator");
|
||||
}
|
||||
|
||||
const code = `${op}(${nestedCode})`;
|
||||
return { code, variables };
|
||||
}
|
||||
case "binop":
|
||||
{
|
||||
const [left, right] = ast.values;
|
||||
const leftResult = astToJs(left);
|
||||
const rightResult = astToJs(right);
|
||||
const op =
|
||||
ast.op === "add" ? "+" :
|
||||
ast.op === "subtract" ? "-" :
|
||||
ast.op === "multiply" ? "*" :
|
||||
ast.op === "divide" ? "/" :
|
||||
ast.op === "exponent" ? "**" :
|
||||
null; // null: never
|
||||
|
||||
if (op === null) {
|
||||
throw new Error("Invalid binary operator");
|
||||
}
|
||||
|
||||
return {
|
||||
code: `(${leftResult.code} ${op} ${rightResult.code})`,
|
||||
variables: new Set([...leftResult.variables, ...rightResult.variables]),
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown ast kind: ${ast.kind}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
import astToJs from "/js/ast-to-js.mjs";
|
||||
import evalCode from "/js/eval-code.mjs";
|
||||
import lex from "/js/lex.mjs";
|
||||
import parse from "/js/parse.mjs";
|
||||
|
||||
const $ = document.querySelector.bind(document);
|
||||
const nameEl = $("#name");
|
||||
const errorEl = $("#error");
|
||||
const reportEl = $("#report");
|
||||
|
||||
const astProgram = $("#program");
|
||||
const program = JSON.parse(astProgram.textContent);
|
||||
|
||||
nameEl.innerText = `Running: ${program.name}`;
|
||||
|
||||
reportEl.addEventListener("click", () => {
|
||||
const file = location.pathname.split("/").slice(-1)[0];
|
||||
reportEl.disabled = true;
|
||||
reportEl.innerText = "Reported!";
|
||||
fetch(`/report`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ file })
|
||||
});
|
||||
})
|
||||
|
||||
try {
|
||||
let ast;
|
||||
if (astProgram.type === "application/x-yaca-code") {
|
||||
const tokens = lex(program.code);
|
||||
ast = parse(tokens);
|
||||
} else {
|
||||
ast = JSON.parse(program.code);
|
||||
}
|
||||
|
||||
const jsProgram = astToJs(ast);
|
||||
evalCode(jsProgram);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
const msg = e instanceof Error ? e.message : "Something went wrong";
|
||||
errorEl.innerText = msg;
|
||||
}
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
const $ = document.querySelector.bind(document);
|
||||
|
||||
const prepareInputs = (variables, onChange) => {
|
||||
const inputEl = $("#input");
|
||||
|
||||
for (const node of [...inputEl.childNodes]) {
|
||||
node.remove();
|
||||
}
|
||||
|
||||
for (const variable of variables) {
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("type", "text");
|
||||
input.setAttribute("placeholder", `${variable.slice(4)}`);
|
||||
input.setAttribute("value", "");
|
||||
input.addEventListener("keyup", () => {
|
||||
let value = Number(input.value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
onChange(variable, value);
|
||||
});
|
||||
inputEl.appendChild(input);
|
||||
}
|
||||
}
|
||||
|
||||
const updateOutput = (result) => {
|
||||
// Float truncation
|
||||
if (typeof result === "number" && Math.floor(result) !== result) {
|
||||
result = result.toFixed(2);
|
||||
}
|
||||
|
||||
const $ = document.querySelector.bind(document);
|
||||
const outputEl = $("#output");
|
||||
|
||||
outputEl.innerText = result;
|
||||
}
|
||||
|
||||
|
||||
export default ({ code, variables }) => {
|
||||
const varList = [...variables].sort((a, b) => a.localeCompare(b));
|
||||
const fn = new Function(...varList, `return (${code});`);
|
||||
const values = new Map(varList.map((val) => [val, 0]));
|
||||
|
||||
const refresh = () => {
|
||||
const result = fn(...varList.map((val) => values.get(val)));
|
||||
|
||||
if (isNaN(result)) {
|
||||
throw new Error("Output was not a number")
|
||||
}
|
||||
|
||||
$("#error").innerText = "";
|
||||
updateOutput(result);
|
||||
}
|
||||
|
||||
const onChange = (variable, value) => {
|
||||
values.set(variable, value);
|
||||
refresh();
|
||||
}
|
||||
|
||||
prepareInputs(varList, onChange);
|
||||
|
||||
refresh();
|
||||
}
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
export default (source) => {
|
||||
let index = 0;
|
||||
const tokens = [];
|
||||
|
||||
while (index < source.length) {
|
||||
const token = source[index];
|
||||
index += 1;
|
||||
|
||||
switch (token) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '\n':
|
||||
case '\r':
|
||||
break;
|
||||
case "~":
|
||||
case "+":
|
||||
case "-":
|
||||
case "*":
|
||||
case "/":
|
||||
case "^": {
|
||||
const op =
|
||||
token === "~" ? "invert" :
|
||||
token === "+" ? "add" :
|
||||
token === "-" ? "subtract" :
|
||||
token === "*" ? "multiply" :
|
||||
token === "/" ? "divide" :
|
||||
token === "^" ? "exponent" :
|
||||
null;
|
||||
|
||||
tokens.push({
|
||||
kind: "operator",
|
||||
value: op,
|
||||
});
|
||||
break
|
||||
}
|
||||
case "(": {
|
||||
tokens.push({
|
||||
kind: "open-paren",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ")": {
|
||||
tokens.push({
|
||||
kind: "close-paren",
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (token.match(/^[0-9\.]$/)) {
|
||||
let currentToken = token;
|
||||
|
||||
while (index < source.length && source[index].match(/^[0-9\.]$/)) {
|
||||
currentToken += source[index];
|
||||
index += 1;
|
||||
}
|
||||
|
||||
const value = Number(currentToken);
|
||||
tokens.push({
|
||||
kind: "number",
|
||||
value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (token.match(/^[a-z]$/)) {
|
||||
let currentToken = token;
|
||||
|
||||
while (index < source.length && source[index].match(/^[a-z0-9_]$/)) {
|
||||
currentToken += source[index];
|
||||
index += 1;
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
kind: "variable",
|
||||
value: currentToken,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Error(`Syntax error: Unexpected "${token}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
const $ = document.querySelector.bind(document);
|
||||
const nameEl = $("#name");
|
||||
const uploadEl = $("#upload");
|
||||
const programEl = $("#program");
|
||||
const contentTypeEl = $("#content-type");
|
||||
|
||||
uploadEl.addEventListener("click", async () => {
|
||||
const code = programEl.value;
|
||||
// We use logical or instead of nullish coalescing because
|
||||
// [input] values are always coerced to a string
|
||||
const name = nameEl.value || nameEl.getAttribute("placeholder");
|
||||
const type = contentTypeEl.value;
|
||||
|
||||
const res = await fetch("/upload", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ type, program: { name, code } })
|
||||
});
|
||||
|
||||
const url = await res.text();
|
||||
window.location.href = url;
|
||||
});
|
||||
|
||||
const staticAst = `{
|
||||
"kind": "binop",
|
||||
"op": "divide",
|
||||
"values": [
|
||||
{
|
||||
"kind": "binop",
|
||||
"op": "add",
|
||||
"values": [
|
||||
{
|
||||
"kind": "unop",
|
||||
"op": "negate",
|
||||
"value": { "kind": "variable", "variable": "b"}
|
||||
},
|
||||
{
|
||||
"kind": "function",
|
||||
"name": "sqrt",
|
||||
"argument": {
|
||||
"kind": "binop",
|
||||
"op": "subtract",
|
||||
"values": [
|
||||
{
|
||||
"kind": "binop",
|
||||
"op": "exponent",
|
||||
"values": [
|
||||
{ "kind": "variable", "variable": "b"},
|
||||
{ "kind": "number", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "binop",
|
||||
"op": "multiply",
|
||||
"values": [
|
||||
{ "kind": "number", "value": 4 },
|
||||
{
|
||||
"kind": "binop",
|
||||
"op": "multiply",
|
||||
"values": [
|
||||
{ "kind": "variable", "variable": "a" },
|
||||
{ "kind": "variable", "variable": "c" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "binop",
|
||||
"op": "multiply",
|
||||
"values": [
|
||||
{ "kind": "number", "value": 2 },
|
||||
{ "kind": "variable", "variable": "a" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}`;
|
||||
|
||||
const staticCode = `(-b + sqrt(b^2 - 4a*c)) / 2a`;
|
||||
|
||||
const resetTextArea = () => {
|
||||
const contentType = contentTypeEl.value;
|
||||
|
||||
if (contentType === "application/x-yaca-ast") {
|
||||
programEl.value = staticAst;
|
||||
} else {
|
||||
programEl.value = staticCode;
|
||||
}
|
||||
}
|
||||
|
||||
contentTypeEl.addEventListener("change", resetTextArea);
|
||||
resetTextArea();
|
||||
Executable
+234
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
Operation precedences:
|
||||
0: exponentiation
|
||||
1: implicit multiplication
|
||||
2: multiplication/division
|
||||
3: unary operators
|
||||
4: addition/subtraction
|
||||
*/
|
||||
|
||||
|
||||
// We could probably have lexed directly into the parse token, but
|
||||
// this way we keep a bit more separation of church & state
|
||||
const lexTokenToParseToken = (token) => {
|
||||
if (token === undefined) {
|
||||
return { kind: "EOF" };
|
||||
}
|
||||
|
||||
switch (token.kind) {
|
||||
case "operator": {
|
||||
switch (token.value) {
|
||||
case "invert": return { kind: "UNOP", value: token.value, precedence: 3, isOp: true };
|
||||
case "subtract": return { kind: "MAYBE_UNOP", value: token.value, unopValue: "negate", precedence: 4, isOp: true };
|
||||
case "exponent": return { kind: "BINOP", value: token.value, precedence: 0, isOp: true, };
|
||||
case "multiply": return { kind: "BINOP", value: token.value, precedence: 2, isOp: true, };
|
||||
case "divide": return { kind: "BINOP", value: token.value, precedence: 2, isOp: true, };
|
||||
case "add": return { kind: "BINOP", value: token.value, precedence: 4, isOp: true, };
|
||||
default: throw new Error(`Unknown operator ${token.value}`);
|
||||
}
|
||||
}
|
||||
case "open-paren": return { kind: "EXPR_START" };
|
||||
case "close-paren": return { kind: "EXPR_END" };
|
||||
case "number": return { kind: "VALUE", ast: { kind: "number", value: token.value } };
|
||||
case "variable": return { kind: "VALUE", ast: { kind: "variable", variable: token.value } };
|
||||
default: throw new Error(`Unknown token kind ${token.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
const mightBeUnop = (token) => {
|
||||
return (
|
||||
token.kind === "UNOP"
|
||||
|| token.kind === "MAYBE_UNOP"
|
||||
|| token.kind === "FUNCTION"
|
||||
|| token.kind === "IMPLICIT_MULTIPLICATION"
|
||||
);
|
||||
}
|
||||
|
||||
const mightBeBinop = (token) => {
|
||||
return (
|
||||
token.kind === "BINOP"
|
||||
|| token.kind === "MAYBE_UNOP"
|
||||
);
|
||||
}
|
||||
|
||||
const parseOne = (stack, lookahead) => {
|
||||
// If we can reduce a parenthetical expression, we want to
|
||||
if (stack[0]?.kind === "EXPR_END") {
|
||||
const [_end, value, _start, ...rest] = stack;
|
||||
|
||||
if (stack[1]?.kind !== "VALUE" || stack[2]?.kind !== "EXPR_START") {
|
||||
throw new Error("Received unexpected close parenthesis");
|
||||
}
|
||||
|
||||
return ["reduce", [value, ...rest]];
|
||||
}
|
||||
|
||||
// Otherwise, all of our reductions occur on values
|
||||
if (stack[0]?.kind === "VALUE") {
|
||||
// We have some special cases for two adjacent values, when the first one is
|
||||
// a pure variable (function call) or number (term multiplication)
|
||||
if (lookahead.kind === "VALUE" || lookahead.kind === "EXPR_START") {
|
||||
if (stack[0].ast.kind === "variable") {
|
||||
const [token, ...rest] = stack;
|
||||
const newToken = {
|
||||
kind: "FUNCTION",
|
||||
isOp: true,
|
||||
precedence: 3,
|
||||
name: token.ast.variable
|
||||
}
|
||||
|
||||
const newStack = [newToken, ...rest];
|
||||
return ["reduce", newStack];
|
||||
}
|
||||
|
||||
if (stack[0].ast.kind === "number") {
|
||||
const [token, ...rest] = stack;
|
||||
const newToken = {
|
||||
kind: "IMPLICIT_MULTIPLICATION",
|
||||
isOp: true,
|
||||
precedence: 1,
|
||||
value: token.ast.value
|
||||
}
|
||||
|
||||
const newStack = [newToken, ...rest];
|
||||
return ["reduce", newStack];
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an operator on our stack, we want to reduce it unless the upcoming
|
||||
// operator has a stronger precedence
|
||||
if (stack[1]?.isOp) {
|
||||
const shouldShift = mightBeBinop(lookahead) && lookahead.precedence < stack[1].precedence;
|
||||
if (shouldShift) {
|
||||
return ["shift"];
|
||||
}
|
||||
|
||||
const isBinop = mightBeBinop(stack[1]) && stack[2]?.kind === "VALUE";
|
||||
|
||||
if (isBinop) {
|
||||
const [right, binop, left, ...rest] = stack;
|
||||
|
||||
// Binops are easy
|
||||
const newValue = {
|
||||
kind: "VALUE",
|
||||
ast: {
|
||||
kind: "binop",
|
||||
op: binop.value,
|
||||
values: [
|
||||
left.ast,
|
||||
right.ast,
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
return ["reduce", [newValue, ...rest]]
|
||||
}
|
||||
|
||||
const isUnop = mightBeUnop(stack[1]);
|
||||
if (!isUnop) {
|
||||
throw new Error("Unexpected operator");
|
||||
}
|
||||
|
||||
const [value, unop, ...rest] = stack;
|
||||
let newToken;
|
||||
|
||||
// Handle each of the different types of unary operators
|
||||
switch (unop.kind) {
|
||||
case "UNOP":
|
||||
case "MAYBE_UNOP": {
|
||||
const op = unop.kind === "MAYBE_UNOP" ? unop.unopValue : unop.value;
|
||||
newToken = {
|
||||
kind: "VALUE",
|
||||
ast: {
|
||||
kind: "unop",
|
||||
op,
|
||||
value: value.ast
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "FUNCTION": {
|
||||
newToken = {
|
||||
kind: "VALUE",
|
||||
ast: {
|
||||
kind: "function",
|
||||
name: unop.name,
|
||||
argument: value.ast,
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "IMPLICIT_MULTIPLICATION": {
|
||||
newToken = {
|
||||
kind: "VALUE",
|
||||
ast: {
|
||||
kind: "binop",
|
||||
op: "multiply",
|
||||
values: [
|
||||
{ kind: "number", value: unop.value },
|
||||
value.ast,
|
||||
]
|
||||
}
|
||||
};
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown unary operator: ${unop.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
return ["reduce", [newToken, ...rest]];
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, we have no reductions to do, so we shift in a new token
|
||||
return ["shift"];
|
||||
}
|
||||
|
||||
|
||||
export default (tokens) => {
|
||||
const queue = [...tokens];
|
||||
let stack = [];
|
||||
|
||||
const maxIter = 1000;
|
||||
let iter = 0;
|
||||
|
||||
while (queue.length > 0 || stack.length > 1) {
|
||||
// I haven't proven that this terminates so uh
|
||||
// Hopefully this will keep me from nuking anyone's chrome
|
||||
if (iter >= maxIter) {
|
||||
throw new Error("Timeout");
|
||||
}
|
||||
iter++;
|
||||
|
||||
const lookahead = lexTokenToParseToken(queue[0]);
|
||||
const action = parseOne(stack, lookahead);
|
||||
|
||||
if (window.DEBUG) {
|
||||
console.log([...stack], lookahead, action);
|
||||
}
|
||||
|
||||
switch (action[0]) {
|
||||
case "shift": {
|
||||
if (lookahead.kind === "EOF") {
|
||||
throw new Error("Attempting to shift EOF, which indicates a malformed program");
|
||||
}
|
||||
|
||||
queue.shift();
|
||||
stack = [lookahead, ...stack]
|
||||
break;
|
||||
}
|
||||
case "reduce": {
|
||||
stack = action[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we parsed correctly, we should be left with a single value
|
||||
// representing our final result
|
||||
if (stack[0]?.kind !== "VALUE") {
|
||||
throw new Error("Parser did not return a value");
|
||||
}
|
||||
|
||||
return stack[0].ast;
|
||||
}
|
||||
Generated
+1996
File diff suppressed because it is too large
Load Diff
Executable
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "yaca",
|
||||
"packageManager": "yarn@3.1.0",
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/puppeteer": "^5.4.5",
|
||||
"@types/sqlite3": "^3.1.8",
|
||||
"@types/uuid": "^8",
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"puppeteer": "^13.5.2",
|
||||
"sqlite3": "^5.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"bundle": "bash -c 'tar -cvzf ../../bundle.tgz .yarn/{plugins,releases} .yarnrc.yml client src Dockerfile package.json tsconfig.json yarn.lock'"
|
||||
}
|
||||
}
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
import * as sqlite from "sqlite3";
|
||||
import * as path from "path";
|
||||
|
||||
const db = new sqlite.Database(path.resolve("./queue.sqlite"));
|
||||
|
||||
const run = <T>(sql: string, params: unknown[]) => {
|
||||
return new Promise<T[]>((resolve, reject) => {
|
||||
db.all(sql, ...params, (err: unknown, rows: T[]) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function setup(){
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS queue (
|
||||
id SERIAL PRIMARY KEY,
|
||||
url TEXT NOT NULL,
|
||||
ip TEXT NOT NULL
|
||||
);
|
||||
`, []);
|
||||
}
|
||||
|
||||
export const enqueue = async (url: string, ip: string) => {
|
||||
const [{ count }] = await run<{ count: number }>(`
|
||||
SELECT count(*) as count
|
||||
FROM queue
|
||||
WHERE ip = ?;
|
||||
`, [ip]);
|
||||
|
||||
if (count > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await run(`
|
||||
INSERT INTO queue (url, ip)
|
||||
VALUES (?, ?);
|
||||
`, [url, ip]);
|
||||
|
||||
const [{ count: queueLength }] = await run<{ count: number }>(`
|
||||
SELECT count(*) as count
|
||||
FROM queue;
|
||||
`, [])
|
||||
|
||||
return queueLength;
|
||||
}
|
||||
|
||||
export const dequeue = async () => {
|
||||
const request = await run<{ url: string, ip: string }>(`
|
||||
SELECT url, ip
|
||||
FROM queue
|
||||
ORDER BY id ASC
|
||||
LIMIT 1;
|
||||
`, []);
|
||||
|
||||
if (request.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [{ url, ip }] = request;
|
||||
|
||||
// Delete based off of url and ip in case there are duplicates
|
||||
await run(`
|
||||
DELETE FROM queue
|
||||
WHERE url = ? AND ip = ?;
|
||||
`, [url, ip]);
|
||||
|
||||
return url;
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
import * as express from "express";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import * as bodyParser from "body-parser";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { startVisiting } from "./page-worker";
|
||||
import { enqueue, setup } from "./database";
|
||||
|
||||
const cacheDir = path.join(__dirname, "../cache");
|
||||
const clientDir = path.join(__dirname, "../client");
|
||||
|
||||
const main = async () => {
|
||||
await setup()
|
||||
await fs.ensureDir(cacheDir);
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader("Content-Security-Policy", "script-src 'self' 'unsafe-eval' 'unsafe-inline'");
|
||||
next();
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(path.join(clientDir, "index.html"));
|
||||
});
|
||||
|
||||
app.post("/upload", async (req, res) => {
|
||||
if (typeof req.body !== "object") {
|
||||
return res.status(500).send("Bad payload");
|
||||
}
|
||||
|
||||
const { type, program } = req.body;
|
||||
if (
|
||||
typeof type !== "string"
|
||||
|| type.match(/^[a-zA-Z\-/]{3,}$/) === null
|
||||
|| typeof program.name !== "string"
|
||||
|| typeof program.code !== "string"
|
||||
|| program.code.length > 10000
|
||||
) {
|
||||
return res.status(500).send("Invalid program");
|
||||
}
|
||||
|
||||
const sanitizedProgram =
|
||||
JSON.stringify(program)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
const template = await fs.readFile(path.join(clientDir, "calculator.hbs"), "utf-8");
|
||||
const formattedFile =
|
||||
template
|
||||
.replace("{{ content-type }}", type)
|
||||
.replace("{{ program }}", sanitizedProgram);
|
||||
|
||||
const fileName = `program-${uuid()}`;
|
||||
await fs.writeFile(path.join(cacheDir, fileName), formattedFile);
|
||||
|
||||
res.send(`/program/${fileName}`);
|
||||
});
|
||||
|
||||
app.post("/report", async (req, res) => {
|
||||
if (
|
||||
typeof req.body !== "object"
|
||||
|| typeof req.body.file !== "string"
|
||||
|| !req.body.file.match(/^program-[a-f0-9-]+$/)
|
||||
) {
|
||||
return res.status(500).send("Bad payload");
|
||||
}
|
||||
|
||||
const ip = req.ip;
|
||||
const { file } = req.body;
|
||||
const url = `http://localhost:3838/program/${file}`;
|
||||
|
||||
await enqueue(url, ip)
|
||||
res.send("Ok");
|
||||
})
|
||||
|
||||
app.get("/program/:file", async (req, res) => {
|
||||
const fileName = req.params.file;
|
||||
const filePath = path.join(cacheDir, fileName);
|
||||
|
||||
res.type("html");
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
app.use("/js", express.static(path.join(clientDir, "js")));
|
||||
app.use("/css", express.static(path.join(clientDir, "css")));
|
||||
|
||||
app.listen(3838, () => {
|
||||
console.log("Listening on port 3838");
|
||||
});
|
||||
}
|
||||
|
||||
startVisiting();
|
||||
main();
|
||||
|
||||
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
import * as puppeteer from "puppeteer";
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
|
||||
import { dequeue } from "./database";
|
||||
|
||||
const flag = process.env.FLAG ?? "flag{missing}"
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const visitOne = async () => {
|
||||
const url = await dequeue();
|
||||
if (url === undefined) {
|
||||
await sleep(500);
|
||||
return;
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
dumpio: true,
|
||||
pipe: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setCookie({
|
||||
name: "flag",
|
||||
value: flag,
|
||||
domain: "localhost:3838",
|
||||
})
|
||||
|
||||
await Promise.race([
|
||||
page.goto(url),
|
||||
sleep(3000),
|
||||
]);
|
||||
await sleep(3000);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
export const startVisiting = async () => {
|
||||
while (true) {
|
||||
try {
|
||||
await visitOne();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"module": "CommonJS",
|
||||
"target": "ES2020",
|
||||
"declaration": true,
|
||||
"incremental": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Plaid CTF: Yet Another Calculator App
|
||||
|
||||
Participant: Peter Millauer / nitowa (01350868)
|
||||
|
||||
## TL;DR / Short Summary
|
||||
|
||||
Classical XSS web exploit. The solution used special string replacement patterns to break out of string escapes.
|
||||
|
||||
## Task Description
|
||||
|
||||
|
||||
|
||||
## Analysis Steps
|
||||
|
||||
Explain your analysis in detail. Cover all the technical aspects, including the used tools and commands. Mention other collaborators and distinguish contributions.
|
||||
|
||||
## Vulnerabilities / Exploitable Issue(s)
|
||||
|
||||
List security issues you discovered in the scope of the task and how they could be exploited.
|
||||
|
||||
## Solution
|
||||
|
||||
Provide a clean (i.e., without analysis and research steps) guideline to get from the task description to the solution. If you did not finish the task, take your most promising approach as a goal.
|
||||
|
||||
## Failed Attempts
|
||||
|
||||
Describe attempts apart from the solution above which you tried. Recap and try to explain why they did not work.
|
||||
|
||||
## Alternative Solutions
|
||||
|
||||
If you can think of an alternative solution (or there are others already published), compare your attempts with those.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
Document what you learned during the competition.
|
||||
|
||||
## References
|
||||
|
||||
List external resources (academic papers, technical blogs, CTF writeups, ...) you used while working on this task.
|
||||
Reference in New Issue
Block a user