Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | 26x 26x 26x 26x 26x 26x 49x 49x 10x 10x 26x 51x 51x 51x 23x 23x 23x 23x 23x 23x 23x 23x 2x 23x 23x 23x 23x 23x 23x 23x 23x 10x 29x 23x 23x 23x 21x 23x 21x 2x 23x 23x | /**
* Sketch File Builder
*
* Handles the construction of Arduino sketch files by wrapping user code
* with the Arduino mock implementation and generating appropriate main() wrappers.
*/
import { join } from "node:path";
import { mkdir, writeFile } from "node:fs/promises";
import { ARDUINO_MOCK_CODE } from "./arduino-mock";
import { Logger } from "@shared/logger";
import { detectSketchEntrypoints } from "@shared/utils/sketch-validation";
import { isSafeHeaderName } from "@shared/input-limits";
import { resolvePathWithinRoot } from "../security/safe-paths";
interface SketchBuildResult {
sketchDir: string;
sketchFile: string;
exeFile: string;
}
export function extractForwardDeclarations(code: string): string {
const stripped = code.replaceAll(/\/\/[^\n]*/g, "").replaceAll(/\/\*[\s\S]*?\*\//g, "");
const skipped = new Set(["if", "else", "while", "for", "do", "switch", "case", "return", "break", "continue", "goto", "class", "struct", "union", "enum", "namespace", "typedef", "setup", "loop", "main"]);
const definition = /^((?:\w[\w*&]*\s+)*\w[\w*&]*)\s+(\w+)\s*(\([^)]*\))\s*\{/gm;
const seen = new Set<string>();
const declarations: string[] = [];
for (const match of stripped.matchAll(definition)) {
const signature = `${match[2]}${match[3]}`;
if (skipped.has(match[2]) || seen.has(signature)) continue;
seen.add(signature);
declarations.push(`${match[1].trim()} ${match[2]}${match[3]};`);
}
return declarations.join("\n");
}
export class SketchFileBuilder {
private readonly logger = new Logger("SketchFileBuilder");
private readonly createdSketchDirs = new Set<string>();
constructor(private readonly tempDir: string) {}
/**
* Builds a complete sketch file with Arduino mock and user code
*
* @param code - User's Arduino code
* @param sketchId - Unique identifier for this sketch
* @returns Paths to sketch directory and files
*/
async build(code: string, sketchId: string, headers: Array<{ name: string; content: string }> = []): Promise<SketchBuildResult> {
const sketchDir = join(this.tempDir, sketchId);
const sketchFile = join(sketchDir, "sketch.cpp");
const exeFile = join(sketchDir, "sketch");
try {
await mkdir(sketchDir, { recursive: true });
this.createdSketchDirs.add(sketchDir);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.logger.error(`Failed to create sketch directory: ${msg}`);
throw err;
}
const { hasSetup, hasLoop } = detectSketchEntrypoints(code);
if (!hasSetup && !hasLoop) {
this.logger.warn(
"Weder setup() noch loop() gefunden - Code wird nur als Bibliothek kompiliert",
);
}
const footer = this.buildFooter(hasSetup, hasLoop);
const cleanedCode = code.replaceAll(/#include\s*[<"]Arduino\.h[>"]/g, "");
const forwardDecls = this.extractForwardDeclarations(cleanedCode);
const forwardSection = forwardDecls
? `// --- Forward declarations (auto-generated, mirrors Arduino IDE behaviour) ---\n${forwardDecls}\n\n`
: "";
const combined = `${ARDUINO_MOCK_CODE}\n\n${forwardSection}// --- User code follows ---\n${cleanedCode}\n\n// --- Footer ---\n${footer}`;
await writeFile(sketchFile, combined);
for (const header of headers) {
if (!isSafeHeaderName(header.name)) throw new Error(`Unsafe header name: ${header.name}`);
await writeFile(resolvePathWithinRoot(sketchDir, header.name), header.content);
}
return { sketchDir, sketchFile, exeFile };
}
getCreatedSketchDirs(): string[] {
return Array.from(this.createdSketchDirs);
}
clearCreatedSketchDir(dir: string): void {
this.createdSketchDirs.delete(dir);
}
/**
* Extracts forward declarations for all user-defined functions.
*
* The Arduino IDE automatically generates prototypes for every function in a
* sketch so that helper functions can be called before they are defined (just
* like in a real Arduino sketch). We replicate that behaviour here so that
* the local g++ compiler accepts the same code the Arduino IDE would accept.
*/
private extractForwardDeclarations(code: string): string {
return extractForwardDeclarations(code);
}
/**
* Generates the main() wrapper based on presence of setup() and loop()
*/
private buildFooter(hasSetup: boolean, hasLoop: boolean): string {
let footer = `
#include <thread>
#include <atomic>
#include <cstring>
#include <chrono>
int main() {
// Initialize IO registry for pin state tracking
initIORegistry();
// Start background thread for serial input
std::thread readerThread(serialInputReader);
readerThread.detach();
`;
if (hasSetup) {
footer += `
// Call user's setup() function
setup();
Serial.flush();
`;
}
if (hasLoop) {
footer += `
// Run user's loop() function continuously
bool __registry_sent = false;
while (1) {
Serial.flush();
loop();
// Send registry after first loop iteration
if (!__registry_sent) {
Serial.flush();
outputIORegistry();
__registry_sent = true;
}
// Sleep 1ms to prevent 100% CPU usage (Arduino runs at ~16MHz, so 1ms is reasonable throttle)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
`;
} else {
footer += `
// No loop() function, just output registry once
outputIORegistry();
`;
}
footer += `
Serial.flush();
// Cleanup: stop serial input reader
keepReading.store(false);
return 0;
}
`;
return footer;
}
}
|