Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions grove.hml
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,120 @@ fn handle_check(body) {
});
}

fn build_bwrap_format_command(temp_file) {
// Build bubblewrap command for formatting (needs write access to temp file)
let format_cmd = `"${CONFIG["hemlock_path"]}" format "${temp_file}"`;

let bwrap_cmd = `"${CONFIG["bwrap_path"]}" ` +
`--unshare-user ` +
`--unshare-net ` +
`--unshare-ipc ` +
`--uid 65534 ` +
`--gid 65534 ` +
`--die-with-parent ` +
`--new-session ` +
`--ro-bind /usr /usr ` +
`--ro-bind /lib /lib ` +
`--ro-bind-try /lib64 /lib64 ` +
`--ro-bind /bin /bin ` +
`--ro-bind-try /usr/local /usr/local ` +
`--dir /var ` +
`--dir /var/tmp ` +
// Need write access for formatting (bind instead of ro-bind)
`--bind "${CONFIG["temp_dir"]}" "${CONFIG["temp_dir"]}" ` +
`--tmpfs /tmp ` +
`--clearenv ` +
`--setenv PATH /usr/local/bin:/usr/bin:/bin ` +
`-- sh -c "${format_cmd}"`;

return bwrap_cmd;
}

fn handle_format(body) {
// Parse JSON body
let data = null;
try {
data = parse(body);
} catch (e) {
return json_response(400, {
success: false,
error: "Invalid JSON body"
});
}

// Get code from body
let code = data["code"];
if (code == null || code == "") {
return json_response(400, {
success: false,
error: "Missing 'code' field"
});
}

// Validate code size
if (code.length > CONFIG["max_code_size"]) {
return json_response(400, {
success: false,
error: "Code exceeds maximum size limit (100KB)"
});
}

// Create temp file with random suffix to prevent predictable filenames
ensure_temp_dir();
let ts = time_ms();
let rand = random_suffix(12);
let temp_file = `${CONFIG["temp_dir"]}/format_${ts}_${rand}.hml`;
write_file(temp_file, code);

// Run hemlock format with optional sandboxing
// hemlock format FILE modifies the file in place
let result = null;
if (CONFIG["secure_mode"]) {
let bwrap_cmd = build_bwrap_format_command(temp_file);
result = run_shell(`timeout 30 ${bwrap_cmd} 2>&1`);
} else {
result = run_shell(`timeout 30 "${CONFIG["hemlock_path"]}" format "${temp_file}" 2>&1`);
}

// Read the formatted code back
let formatted_code = "";
let format_error = "";

if (result["code"] == 0) {
// Success - read the formatted file
if (exists(temp_file)) {
formatted_code = read_file(temp_file);
}
} else {
// Format failed - get error message
format_error = result["stdout"];
if ((format_error == "" || format_error == null) && result["stderr"] != null) {
format_error = result["stderr"];
}
// Clean up temp file path from error messages
if (format_error != "" && format_error != null) {
format_error = format_error.split(temp_file).join("main.hml");
}
}

// Cleanup temp file
if (exists(temp_file)) {
remove_file(temp_file);
}

if (result["code"] == 0) {
return json_response(200, {
success: true,
formatted: formatted_code
});
} else {
return json_response(200, {
success: false,
error: format_error
});
}
}

fn handle_execute(body) {
// Parse JSON body
let data = null;
Expand Down Expand Up @@ -946,6 +1060,8 @@ fn handle_connection(stream) {
response = handle_execute(req["body"]);
} else if (path == "/check" && method == "POST") {
response = handle_check(req["body"]);
} else if (path == "/format" && method == "POST") {
response = handle_format(req["body"]);
} else {
response = handle_not_found(path);
}
Expand Down Expand Up @@ -996,6 +1112,7 @@ fn main() {
print(" GET /health - Health check");
print(" GET /version - Version info");
print(" POST /check - Type check code (hemlockc)");
print(" POST /format - Format code (hemlock fmt)");
print(" POST /execute - Execute code");
print("");
print("Press Ctrl+C to stop.");
Expand Down
54 changes: 52 additions & 2 deletions playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@
<button data-example="input">User Input (stdin)</button>
</div>
</div>
<button id="format-btn">{ } Format</button>
<button id="check-btn">✓ Check</button>
<button id="run-btn" class="primary">▶ Run</button>
</div>
Expand Down Expand Up @@ -866,6 +867,7 @@
const output = document.getElementById('output');
const runBtn = document.getElementById('run-btn');
const checkBtn = document.getElementById('check-btn');
const formatBtn = document.getElementById('format-btn');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const versionInfo = document.getElementById('version-info');
Expand Down Expand Up @@ -997,7 +999,7 @@
// Check syntax
async function checkSyntax() {
if (isRunning) return;

const code = editor.value;
if (!code.trim()) {
output.innerHTML = '<span class="output-info">No code to check</span>';
Expand All @@ -1014,7 +1016,7 @@
});

const data = await res.json();

if (data.valid) {
output.innerHTML = '<span class="output-success">✓ Syntax OK</span>';
setStatus('Valid');
Expand All @@ -1029,6 +1031,53 @@
}
}

// Format code
async function formatCode() {
if (isRunning) return;

const code = editor.value;
if (!code.trim()) {
output.innerHTML = '<span class="output-info">No code to format</span>';
return;
}

isRunning = true;
runBtn.disabled = true;
checkBtn.disabled = true;
formatBtn.disabled = true;
setStatus('Formatting...', false, true);

try {
const res = await fetch(`${API_URL}/format`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});

const data = await res.json();

if (data.success) {
// Update editor with formatted code
editor.value = data.formatted;
updateHighlight();
output.innerHTML = '<span class="output-success">✓ Code formatted</span>';
setStatus('Formatted');
} else {
output.innerHTML = `<span class="output-error">Format error: ${escapeHtml(data.error)}</span>`;
setStatus('Format Error', true);
}

} catch (e) {
output.innerHTML = `<span class="output-error">Failed to connect to API: ${e.message}</span>`;
setStatus('API Error', true);
} finally {
isRunning = false;
runBtn.disabled = false;
checkBtn.disabled = false;
formatBtn.disabled = false;
}
}

// Load example
function loadExample(name) {
if (EXAMPLES[name]) {
Expand Down Expand Up @@ -1058,6 +1107,7 @@
// Event listeners
runBtn.addEventListener('click', runCode);
checkBtn.addEventListener('click', checkSyntax);
formatBtn.addEventListener('click', formatCode);

// Syntax highlighting events
editor.addEventListener('input', updateHighlight);
Expand Down