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