wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ICHandler.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QDir>
7#include <QFile>
8#include <QFileInfo>
9#include <QJsonArray>
10
11#include "App/Core/Constants.h"
12#include "App/Element/IC.h"
14#include "App/Scene/Commands.h"
16#include "App/Scene/Scene.h"
17#include "App/Scene/Workspace.h"
18#include "App/UI/MainWindow.h"
19
20namespace {
21
26bool isBareFileName(const QString &name)
27{
28 return name != "." && name != ".." && QFileInfo(name).fileName() == name
29 && !name.contains('/') && !name.contains('\\');
30}
31
32} // namespace
33
34ICHandler::ICHandler(MainWindow *mainWindow, const MCPValidator *validator)
35 : BaseHandler(mainWindow, validator)
36{
37}
38
39QJsonObject ICHandler::handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId)
40{
41 if (command == "create_ic") {
42 return handleCreateIC(params, requestId);
43 } else if (command == "instantiate_ic") {
44 return handleInstantiateIC(params, requestId);
45 } else if (command == "list_ics") {
46 return handleListICs(params, requestId);
47 } else if (command == "embed_ic") {
48 return handleEmbedIC(params, requestId);
49 } else if (command == "extract_ic") {
50 return handleExtractIC(params, requestId);
51 } else {
52 return createErrorResponse(QString("Unknown IC command: %1").arg(command),
54 }
55}
56
57QJsonObject ICHandler::handleCreateIC(const QJsonObject &params, const QJsonValue &requestId)
58{
59 if (!validateParameters(params, {"name"})) {
60 return createErrorResponse("Missing required parameter: name", requestId, JsonRpcError::InvalidParams);
61 }
62
63 Scene *scene = currentScene();
64 if (!scene) {
65 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
66 }
67
68 QString name = params.value("name").toString();
69 QString description = params.value("description").toString("");
70
71 if (name.isEmpty()) {
72 return createErrorResponse("IC name cannot be empty", requestId, JsonRpcError::InvalidParams);
73 }
74
75 if (!isBareFileName(name)) {
76 return createErrorResponse("IC name must not contain path separators or directory components",
78 }
79
80 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
81 QString icFileName = name + ".panda";
82 QString fullPath = m_mainWindow->currentDir().absoluteFilePath(icFileName);
83
84 if (QFile::exists(fullPath)) {
85 return createErrorResponse(QString("IC file already exists: %1").arg(icFileName),
86 requestId, JsonRpcError::IcError);
87 }
88
89 const auto elements = scene->elements();
90 if (elements.isEmpty()) {
91 return createErrorResponse("Cannot create IC from empty circuit", requestId, JsonRpcError::IcError);
92 }
93
94 // Unreachable: currentScene() above already returned non-null, which (per its own
95 // implementation) requires m_mainWindow->currentTab() to be non-null -- nothing
96 // between there and here can close the current tab.
97 auto *workspace = m_mainWindow->currentTab();
98 if (!workspace) {
99 return createErrorResponse("No active workspace available", requestId, JsonRpcError::InternalError); // LCOV_EXCL_LINE
100 }
101
102 // Unreachable in any real MCP invocation: Main.cpp sets Application::interactiveMode
103 // = false unconditionally before ICHandler is ever constructed for --mcp-mode/
104 // --mcp-gui, and WorkSpace::save() only ever returns SaveOutcome::ReadOnlyTarget when
105 // Application::interactiveMode is true -- an open failure here always throws instead
106 // (caught generically by tryCommand, see testHandleCreateIcFailsOnUnwritableTarget).
107 if (workspace->save(fullPath) == WorkSpace::SaveOutcome::ReadOnlyTarget) {
108 return createErrorResponse(QString("Cannot write IC file (target location is read-only): %1").arg(fullPath), // LCOV_EXCL_LINE
109 requestId, JsonRpcError::IcError); // LCOV_EXCL_LINE
110 }
111
112 QJsonObject result;
113 result["name"] = name;
114 result["filename"] = icFileName;
115 result["path"] = fullPath;
116 result["elements_count"] = elements.size();
117 result["description"] = description;
118 result["message"] = "IC created successfully from current circuit";
119
120 return createSuccessResponse(result, requestId);
121 }, "create IC", requestId);
122}
123
124QJsonObject ICHandler::handleInstantiateIC(const QJsonObject &params, const QJsonValue &requestId)
125{
126 if (!validateParameters(params, {"ic_name", "x", "y"})) {
127 return createErrorResponse("Missing required parameters: ic_name, x, y", requestId, JsonRpcError::InvalidParams);
128 }
129
130 Scene *scene = currentScene();
131 if (!scene) {
132 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
133 }
134
135 QString icName = params.value("ic_name").toString();
136 if (icName.isEmpty()) {
137 return createErrorResponse("Parameter 'ic_name' cannot be empty", requestId, JsonRpcError::InvalidParams);
138 }
139
140 // Two ways to reference a dependency: a bare name (interactive/GUI use, resolved
141 // against the current tab's directory) or an absolute path (automation clients,
142 // e.g. the Python IC generators, which run one headless process per script with
143 // no reliable "current directory" to resolve a bare name against). Absolute paths
144 // still can't contain '..' components, so this doesn't relax path-traversal safety.
145 const bool isAbsolute = QFileInfo(icName).isAbsolute();
146 if (isAbsolute) {
147 if (icName.contains("..")) {
148 return createErrorResponse("IC path must not contain '..' components",
149 requestId, JsonRpcError::InvalidParams);
150 }
151 } else if (!isBareFileName(icName)) {
152 return createErrorResponse("IC name must not contain path separators or directory components",
153 requestId, JsonRpcError::InvalidParams);
154 }
155
156 const int snap = Constants::gridSize / 2;
157 int x = qRound(params.value("x").toDouble() / snap) * snap;
158 int y = qRound(params.value("y").toDouble() / snap) * snap;
159 QString label = params.value("label").toString(icName);
160
161 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
162 QString icFileName = icName + ".panda";
163 QString fullPath = isAbsolute ? icFileName : m_mainWindow->currentDir().absoluteFilePath(icFileName);
164
165 if (!QFile::exists(fullPath)) {
166 return createErrorResponse(QString("IC file not found: %1").arg(icFileName),
167 requestId, JsonRpcError::IcError);
168 }
169
170 auto ic = std::make_unique<IC>();
171
172 const QString icDirectory = QFileInfo(fullPath).absolutePath();
173 const bool inlineMode = params.value("inline").toBool(false);
174
175 IC *icPtr = nullptr;
176
177 if (inlineMode) {
178 QFile file(fullPath);
179 if (!file.open(QIODevice::ReadOnly)) {
180 return createErrorResponse(QString("Could not read IC file: %1").arg(file.errorString()),
181 requestId, JsonRpcError::FileError);
182 }
183 QByteArray fileBytes = file.readAll();
184 file.close();
185
186 QString blobName = params.value("blob_name").toString();
187 if (blobName.isEmpty()) {
188 blobName = QFileInfo(fullPath).baseName();
189 } else if (!isBareFileName(blobName)) {
190 // Same reasoning as handleEmbedIC: this blob_name is stored verbatim as a
191 // registry key and can later be used as a file-name fallback by extract_ic.
192 return createErrorResponse("blob_name must not contain path separators or directory components",
193 requestId, JsonRpcError::InvalidParams);
194 }
195
196 auto *reg = scene->icRegistry();
197 if (reg->hasBlob(blobName)) {
198 return createErrorResponse(QString("Blob name collision: an embedded IC named '%1' already exists. " // LCOV_EXCL_LINE -- pattern 8: gcov misattributes this multi-line chained string/.arg() call's first line even though it's genuinely reached (see testHandleInstantiateIcInlineRejectsBlobNameCollision)
199 "Use blob_name parameter to specify a different name.").arg(blobName),
200 requestId, JsonRpcError::IcError);
201 }
202
203 icPtr = reg->createEmbeddedIC(blobName, fileBytes, icDirectory);
204 } else {
205 // loadFile() can throw (nesting depth, circular reference, bad file); release()
206 // only after it succeeds so the unique_ptr cleans up on the throwing path instead
207 // of leaking, mirroring SceneDropHandler.cpp's identical loadFromDrop() sequencing.
208 ic->loadFile(fullPath, icDirectory);
209 icPtr = ic.release();
210 scene->receiveCommand(new AddItemsCommand({icPtr}, scene));
211 }
212
213 icPtr->setPos(x, y);
214 icPtr->setLabel(label);
215
216 const QRectF bounds = icPtr->boundingRect();
217
218 QJsonObject result;
219 result["element_id"] = icPtr->id();
220 result["ic_name"] = icName;
221 result["filename"] = icFileName;
222 result["label"] = label;
223 result["position"] = QJsonObject{{"x", x}, {"y", y}};
224 result["input_count"] = icPtr->inputSize();
225 result["output_count"] = icPtr->outputSize();
226 result["width"] = bounds.width();
227 result["height"] = bounds.height();
228 if (inlineMode) {
229 result["inline"] = true;
230 result["blob_name"] = icPtr->blobName();
231 }
232 result["message"] = "IC instantiated successfully";
233
234 return createSuccessResponse(result, requestId);
235 }, "instantiate IC", requestId);
236}
237
238QJsonObject ICHandler::handleListICs(const QJsonObject &, const QJsonValue &requestId)
239{
240 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
241 QJsonArray icsArray;
242
243 QDir currentDir(m_mainWindow->currentDir());
244 QStringList filters;
245 filters << "*.panda";
246
247 const QFileInfoList pandaFiles = currentDir.entryInfoList(filters, QDir::Files);
248
249 for (const QFileInfo &fileInfo : pandaFiles) {
250 try {
251 QFile file(fileInfo.absoluteFilePath());
252 if (!file.open(QIODevice::ReadOnly)) {
253 continue;
254 }
255
256 QDataStream stream(&file);
257 QVersionNumber version = Serialization::readPandaHeader(stream);
258
259 if (!version.isNull()) {
260 QJsonObject icInfo;
261 icInfo["name"] = fileInfo.baseName();
262 icInfo["filename"] = fileInfo.fileName();
263 icInfo["path"] = fileInfo.absoluteFilePath();
264 icInfo["size"] = fileInfo.size();
265 icInfo["modified"] = fileInfo.lastModified().toString(Qt::ISODate);
266
267 try {
268 IC tempIC;
269 tempIC.loadFile(fileInfo.absoluteFilePath(), fileInfo.absolutePath());
270 icInfo["input_count"] = tempIC.inputSize();
271 icInfo["output_count"] = tempIC.outputSize();
272 icInfo["has_valid_definition"] = true;
273 } catch (...) {
274 icInfo["has_valid_definition"] = false;
275 icInfo["input_count"] = 0;
276 icInfo["output_count"] = 0;
277 }
278
279 icsArray.append(icInfo);
280 }
281
282 file.close();
283
284 } catch (...) {
285 continue;
286 }
287 }
288
289 QJsonObject result;
290 result["ics"] = icsArray;
291 result["directory"] = currentDir.absolutePath();
292 result["count"] = icsArray.size();
293
294 return createSuccessResponse(result, requestId);
295 }, "list ICs", requestId);
296}
297
298QJsonObject ICHandler::handleEmbedIC(const QJsonObject &params, const QJsonValue &requestId)
299{
300 if (!validateParameters(params, {"element_id"})) {
301 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
302 }
303
304 Scene *scene = currentScene();
305 if (!scene) {
306 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
307 }
308
309 int elementId = params.value("element_id").toInt();
310 if (elementId <= 0) {
311 return createErrorResponse("element_id must be a positive integer", requestId, JsonRpcError::InvalidParams);
312 }
313
314 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
315 auto *item = scene->itemById(elementId);
316 if (!item) {
317 return createErrorResponse(QString("Element with ID %1 not found").arg(elementId),
319 }
320
321 auto *elm = dynamic_cast<GraphicElement *>(item);
322 if (!elm || elm->elementType() != ElementType::IC) {
323 return createErrorResponse("Element is not an IC", requestId, JsonRpcError::ValidationError);
324 }
325
326 auto *ic = static_cast<IC *>(elm);
327
328 if (ic->isEmbedded()) {
329 return createErrorResponse("IC is already embedded", requestId, JsonRpcError::IcError);
330 }
331
332 if (ic->file().isEmpty()) {
333 return createErrorResponse("IC has no referenced file", requestId, JsonRpcError::IcError);
334 }
335
336 const QString contextDir = scene->contextDir();
337 if (contextDir.isEmpty()) {
338 return createErrorResponse("Project must be saved before embedding ICs", requestId, JsonRpcError::IcError);
339 }
340
341 const QString resolvedPath = QDir(contextDir).absoluteFilePath(ic->file());
342 QFile file(resolvedPath);
343 if (!file.open(QIODevice::ReadOnly)) {
344 return createErrorResponse(QString("Cannot read IC file: %1").arg(file.errorString()),
345 requestId, JsonRpcError::FileError);
346 }
347 QByteArray fileBytes = file.readAll();
348 file.close();
349
350 QString blobName = params.value("blob_name").toString();
351 if (blobName.isEmpty()) {
352 blobName = QFileInfo(resolvedPath).baseName();
353 } else if (!isBareFileName(blobName)) {
354 // blob_name is later used verbatim as a file-name fallback by extract_ic
355 // (ICHandler::handleExtractIC) — reject path traversal here too, not just there,
356 // so a malicious name can't ride through the registry as a stored blob key.
357 return createErrorResponse("blob_name must not contain path separators or directory components",
358 requestId, JsonRpcError::InvalidParams);
359 }
360
361 auto *reg = scene->icRegistry();
362 if (reg->hasBlob(blobName)) {
363 return createErrorResponse(QString("Blob name collision: an embedded IC named '%1' already exists").arg(blobName),
364 requestId, JsonRpcError::IcError);
365 }
366
367 const int count = reg->embedICsByFile(ic->file(), fileBytes, blobName);
368
369 QJsonObject result;
370 result["blob_name"] = blobName;
371 result["converted_count"] = count;
372 result["input_count"] = ic->inputSize();
373 result["output_count"] = ic->outputSize();
374 result["blob_size"] = fileBytes.size();
375 result["message"] = QString("Embedded %1 IC(s) as '%2'").arg(count).arg(blobName);
376
377 return createSuccessResponse(result, requestId);
378 }, "embed IC", requestId);
379}
380
381QJsonObject ICHandler::handleExtractIC(const QJsonObject &params, const QJsonValue &requestId)
382{
383 if (!validateParameters(params, {"blob_name"})) {
384 return createErrorResponse("Missing required parameter: blob_name", requestId, JsonRpcError::InvalidParams);
385 }
386
387 Scene *scene = currentScene();
388 if (!scene) {
389 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
390 }
391
392 const QString contextDir = scene->contextDir();
393 if (contextDir.isEmpty()) {
394 return createErrorResponse("Project must be saved before extracting ICs", requestId, JsonRpcError::IcError);
395 }
396
397 const QString blobName = params.value("blob_name").toString();
398 if (blobName.isEmpty()) {
399 return createErrorResponse("blob_name must not be empty", requestId, JsonRpcError::InvalidParams);
400 }
401
402 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
403 auto *reg = scene->icRegistry();
404 if (!reg->hasBlob(blobName)) {
405 return createErrorResponse(QString("No embedded IC with blob name '%1' found").arg(blobName),
406 requestId, JsonRpcError::IcError);
407 }
408
409 QString fileName = params.value("file_name").toString();
410 if (fileName.isEmpty()) {
411 fileName = QDir(contextDir).absoluteFilePath(blobName + ".panda");
412 } else if (QFileInfo(fileName).isRelative()) {
413 fileName = QDir(contextDir).absoluteFilePath(fileName);
414 }
415 if (!fileName.endsWith(".panda")) {
416 fileName.append(".panda");
417 }
418
419 // file_name is an MCP-client-supplied identifier, not a GUI-picked path (unlike
420 // ICController::extractSelectedIC, which always routes through an interactive
421 // getSaveFileName dialog a human must approve) — an absolute file_name or one with
422 // ".." components would otherwise let the write above land anywhere on disk. Validate
423 // the final resolved path rather than the individual inputs that built it, so this
424 // can't be bypassed by some other future parameter combination; subdirectories of
425 // contextDir remain allowed, only escapes are rejected.
426 const QString cleanContextDir = QDir::cleanPath(QDir(contextDir).absolutePath());
427 const QString cleanFileName = QDir::cleanPath(fileName);
428 if (cleanFileName != cleanContextDir && !cleanFileName.startsWith(cleanContextDir + '/')) {
429 return createErrorResponse("file_name must resolve to a location inside the project directory",
430 requestId, JsonRpcError::InvalidParams);
431 }
432 fileName = cleanFileName;
433
434 if (QFile::exists(fileName) && !params.value("overwrite").toBool()) {
435 return createErrorResponse(QString("File '%1' already exists. Set overwrite=true to replace it.").arg(fileName),
436 requestId, JsonRpcError::FileError);
437 }
438
439 const int count = reg->extractToFile(blobName, fileName);
440
441 QJsonObject result;
442 result["blob_name"] = blobName;
443 result["file_name"] = fileName;
444 result["converted_count"] = count;
445 result["message"] = QString("Extracted %1 IC(s) to '%2'").arg(count).arg(fileName);
446
447 return createSuccessResponse(result, requestId);
448 }, "extract IC", requestId);
449}
All QUndoCommand subclasses and the CommandUtils helper namespace.
Shared numeric constants used across layers.
IC definition registry with file watching and embedded blob storage.
Integrated Circuit (IC) graphic element that encapsulates a sub-circuit file.
Main application window providing menus, toolbars, and tab management.
Main circuit editing scene with undo/redo and user interaction.
Circuit and waveform file serialization/deserialization utilities.
WorkSpace widget: the complete circuit editing environment for one tab.
QJsonObject createSuccessResponse(const QJsonObject &result={}, const QJsonValue &requestId=QJsonValue()) const
QJsonObject tryCommand(Fn &&fn, const QString &action, const QJsonValue &requestId=QJsonValue())
Wraps fn in a try/catch, returning an error response on exception.
Definition BaseHandler.h:55
BaseHandler(MainWindow *mainWindow, const MCPValidator *validator)
QJsonObject createErrorResponse(const QString &error, const QJsonValue &requestId=QJsonValue(), int code=JsonRpcError::InternalError) const
MainWindow * m_mainWindow
Definition BaseHandler.h:85
bool validateParameters(const QJsonObject &params, const QStringList &required) const
Scene * currentScene()
int inputSize() const
Returns the current number of input ports.
void setLabel(const QString &label)
Sets the label text to label and refreshes the display.
int outputSize() const
Returns the current number of output ports.
ICHandler(MainWindow *mainWindow, const MCPValidator *validator)
Definition ICHandler.cpp:34
QJsonObject handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId) override
Definition ICHandler.cpp:39
int embedICsByFile(const QString &fileName, const QByteArray &fileBytes, const QString &blobName)
Converts all file-backed IC elements referencing fileName to embedded ICs using blobName.
const QString & blobName() const override
Returns the blob name for embedded ICs, empty if file-backed.
Definition IC.h:80
QRectF boundingRect() const override
Definition IC.cpp:322
void loadFile(const QString &fileName, const QString &contextDir={})
Loads the IC circuit from fileName and rebuilds the logic mapping.
Definition IC.cpp:243
int id() const
Returns the unique integer identifier of this item, or -1 if unassigned.
Definition ItemWithId.h:40
JSON Schema validator for MCP commands and responses using native json-schema-validator.
The top-level application window hosting the tab bar, menus, element palette, and editor.
Definition MainWindow.h:46
const QVector< GraphicElement * > elements() const
Returns all graphic elements in the scene.
Definition Scene.cpp:336
void receiveCommand(QUndoCommand *cmd)
Pushes cmd onto the undo stack (immediately executes its redo()).
Definition Scene.cpp:520
QString contextDir() const override
Returns the directory of the .panda file associated with this scene.
Definition Scene.h:358
ItemWithId * itemById(int id) const
Returns the item registered under id, or nullptr if not found.
Definition Scene.cpp:171
ICRegistry * icRegistry()
Returns the IC definition registry for this scene.
Definition Scene.h:365
static QVersionNumber readPandaHeader(QDataStream &stream)
Reads and validates the .panda circuit file header; returns the stored version number.
constexpr int gridSize
Scene grid unit in pixels (elements snap to gridSize/2).
Definition Constants.h:12
constexpr int SceneNotAvailable
No active circuit scene to operate on.
constexpr int MethodNotFound
The requested method does not exist or is unavailable.
constexpr int InternalError
Internal JSON-RPC error (last-resort).
constexpr int ElementNotFound
Referenced element id does not exist in the scene.
constexpr int FileError
File save/load/path error.
constexpr int ValidationError
Semantic validation failure (e.g. port index out of range, enum value not allowed).
constexpr int InvalidParams
Invalid method parameters (missing required, wrong type, etc.).
constexpr int IcError
IC create / instantiate / embed / extract failure.