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 {
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 auto *workspace = m_mainWindow->currentTab();
95 if (!workspace) {
96 return createErrorResponse("No active workspace available", requestId, JsonRpcError::InternalError);
97 }
98
99 if (workspace->save(fullPath) == WorkSpace::SaveOutcome::ReadOnlyTarget) {
100 return createErrorResponse(QString("Cannot write IC file (target location is read-only): %1").arg(fullPath),
101 requestId, JsonRpcError::IcError);
102 }
103
104 QJsonObject result;
105 result["name"] = name;
106 result["filename"] = icFileName;
107 result["path"] = fullPath;
108 result["elements_count"] = elements.size();
109 result["description"] = description;
110 result["message"] = "IC created successfully from current circuit";
111
112 return createSuccessResponse(result, requestId);
113 }, "create IC", requestId);
114}
115
116QJsonObject ICHandler::handleInstantiateIC(const QJsonObject &params, const QJsonValue &requestId)
117{
118 if (!validateParameters(params, {"ic_name", "x", "y"})) {
119 return createErrorResponse("Missing required parameters: ic_name, x, y", requestId, JsonRpcError::InvalidParams);
120 }
121
122 Scene *scene = currentScene();
123 if (!scene) {
124 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
125 }
126
127 QString icName = params.value("ic_name").toString();
128 if (icName.isEmpty()) {
129 return createErrorResponse("Parameter 'ic_name' cannot be empty", requestId, JsonRpcError::InvalidParams);
130 }
131
132 // Two ways to reference a dependency: a bare name (interactive/GUI use, resolved
133 // against the current tab's directory) or an absolute path (automation clients,
134 // e.g. the Python IC generators, which run one headless process per script with
135 // no reliable "current directory" to resolve a bare name against). Absolute paths
136 // still can't contain '..' components, so this doesn't relax path-traversal safety.
137 const bool isAbsolute = QFileInfo(icName).isAbsolute();
138 if (isAbsolute) {
139 if (icName.contains("..")) {
140 return createErrorResponse("IC path must not contain '..' components",
141 requestId, JsonRpcError::InvalidParams);
142 }
143 } else if (!isBareFileName(icName)) {
144 return createErrorResponse("IC name must not contain path separators or directory components",
145 requestId, JsonRpcError::InvalidParams);
146 }
147
148 const int snap = Constants::gridSize / 2;
149 int x = qRound(params.value("x").toDouble() / snap) * snap;
150 int y = qRound(params.value("y").toDouble() / snap) * snap;
151 QString label = params.value("label").toString(icName);
152
153 return tryCommand([&]() -> QJsonObject {
154 QString icFileName = icName + ".panda";
155 QString fullPath = isAbsolute ? icFileName : m_mainWindow->currentDir().absoluteFilePath(icFileName);
156
157 if (!QFile::exists(fullPath)) {
158 return createErrorResponse(QString("IC file not found: %1").arg(icFileName),
159 requestId, JsonRpcError::IcError);
160 }
161
162 auto ic = std::make_unique<IC>();
163
164 const QString icDirectory = QFileInfo(fullPath).absolutePath();
165 const bool inlineMode = params.value("inline").toBool(false);
166
167 IC *icPtr = nullptr;
168
169 if (inlineMode) {
170 QFile file(fullPath);
171 if (!file.open(QIODevice::ReadOnly)) {
172 return createErrorResponse(QString("Could not read IC file: %1").arg(file.errorString()),
173 requestId, JsonRpcError::FileError);
174 }
175 QByteArray fileBytes = file.readAll();
176 file.close();
177
178 QString blobName = params.value("blob_name").toString();
179 if (blobName.isEmpty()) {
180 blobName = QFileInfo(fullPath).baseName();
181 } else if (!isBareFileName(blobName)) {
182 // Same reasoning as handleEmbedIC: this blob_name is stored verbatim as a
183 // registry key and can later be used as a file-name fallback by extract_ic.
184 return createErrorResponse("blob_name must not contain path separators or directory components",
185 requestId, JsonRpcError::InvalidParams);
186 }
187
188 auto *reg = scene->icRegistry();
189 if (reg->hasBlob(blobName)) {
190 return createErrorResponse(QString("Blob name collision: an embedded IC named '%1' already exists. "
191 "Use blob_name parameter to specify a different name.").arg(blobName),
192 requestId, JsonRpcError::IcError);
193 }
194
195 icPtr = reg->createEmbeddedIC(blobName, fileBytes, icDirectory);
196 } else {
197 // loadFile() can throw (nesting depth, circular reference, bad file); release()
198 // only after it succeeds so the unique_ptr cleans up on the throwing path instead
199 // of leaking, mirroring SceneDropHandler.cpp's identical loadFromDrop() sequencing.
200 ic->loadFile(fullPath, icDirectory);
201 icPtr = ic.release();
202 scene->receiveCommand(new AddItemsCommand({icPtr}, scene));
203 }
204
205 icPtr->setPos(x, y);
206 icPtr->setLabel(label);
207
208 const QRectF bounds = icPtr->boundingRect();
209
210 QJsonObject result;
211 result["element_id"] = icPtr->id();
212 result["ic_name"] = icName;
213 result["filename"] = icFileName;
214 result["label"] = label;
215 result["position"] = QJsonObject{{"x", x}, {"y", y}};
216 result["input_count"] = icPtr->inputSize();
217 result["output_count"] = icPtr->outputSize();
218 result["width"] = bounds.width();
219 result["height"] = bounds.height();
220 if (inlineMode) {
221 result["inline"] = true;
222 result["blob_name"] = icPtr->blobName();
223 }
224 result["message"] = "IC instantiated successfully";
225
226 return createSuccessResponse(result, requestId);
227 }, "instantiate IC", requestId);
228}
229
230QJsonObject ICHandler::handleListICs(const QJsonObject &, const QJsonValue &requestId)
231{
232 return tryCommand([&]() -> QJsonObject {
233 QJsonArray icsArray;
234
235 QDir currentDir(m_mainWindow->currentDir());
236 QStringList filters;
237 filters << "*.panda";
238
239 const QFileInfoList pandaFiles = currentDir.entryInfoList(filters, QDir::Files);
240
241 for (const QFileInfo &fileInfo : pandaFiles) {
242 try {
243 QFile file(fileInfo.absoluteFilePath());
244 if (!file.open(QIODevice::ReadOnly)) {
245 continue;
246 }
247
248 QDataStream stream(&file);
249 QVersionNumber version = Serialization::readPandaHeader(stream);
250
251 if (!version.isNull()) {
252 QJsonObject icInfo;
253 icInfo["name"] = fileInfo.baseName();
254 icInfo["filename"] = fileInfo.fileName();
255 icInfo["path"] = fileInfo.absoluteFilePath();
256 icInfo["size"] = fileInfo.size();
257 icInfo["modified"] = fileInfo.lastModified().toString(Qt::ISODate);
258
259 try {
260 IC tempIC;
261 tempIC.loadFile(fileInfo.absoluteFilePath(), fileInfo.absolutePath());
262 icInfo["input_count"] = tempIC.inputSize();
263 icInfo["output_count"] = tempIC.outputSize();
264 icInfo["has_valid_definition"] = true;
265 } catch (...) {
266 icInfo["has_valid_definition"] = false;
267 icInfo["input_count"] = 0;
268 icInfo["output_count"] = 0;
269 }
270
271 icsArray.append(icInfo);
272 }
273
274 file.close();
275
276 } catch (...) {
277 continue;
278 }
279 }
280
281 QJsonObject result;
282 result["ics"] = icsArray;
283 result["directory"] = currentDir.absolutePath();
284 result["count"] = icsArray.size();
285
286 return createSuccessResponse(result, requestId);
287 }, "list ICs", requestId);
288}
289
290QJsonObject ICHandler::handleEmbedIC(const QJsonObject &params, const QJsonValue &requestId)
291{
292 if (!validateParameters(params, {"element_id"})) {
293 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
294 }
295
296 Scene *scene = currentScene();
297 if (!scene) {
298 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
299 }
300
301 int elementId = params.value("element_id").toInt();
302 if (elementId <= 0) {
303 return createErrorResponse("element_id must be a positive integer", requestId, JsonRpcError::InvalidParams);
304 }
305
306 return tryCommand([&]() -> QJsonObject {
307 auto *item = scene->itemById(elementId);
308 if (!item) {
309 return createErrorResponse(QString("Element with ID %1 not found").arg(elementId),
311 }
312
313 auto *elm = dynamic_cast<GraphicElement *>(item);
314 if (!elm || elm->elementType() != ElementType::IC) {
315 return createErrorResponse("Element is not an IC", requestId, JsonRpcError::ValidationError);
316 }
317
318 auto *ic = static_cast<IC *>(elm);
319
320 if (ic->isEmbedded()) {
321 return createErrorResponse("IC is already embedded", requestId, JsonRpcError::IcError);
322 }
323
324 if (ic->file().isEmpty()) {
325 return createErrorResponse("IC has no referenced file", requestId, JsonRpcError::IcError);
326 }
327
328 const QString contextDir = scene->contextDir();
329 if (contextDir.isEmpty()) {
330 return createErrorResponse("Project must be saved before embedding ICs", requestId, JsonRpcError::IcError);
331 }
332
333 const QString resolvedPath = QDir(contextDir).absoluteFilePath(ic->file());
334 QFile file(resolvedPath);
335 if (!file.open(QIODevice::ReadOnly)) {
336 return createErrorResponse(QString("Cannot read IC file: %1").arg(file.errorString()),
337 requestId, JsonRpcError::FileError);
338 }
339 QByteArray fileBytes = file.readAll();
340 file.close();
341
342 QString blobName = params.value("blob_name").toString();
343 if (blobName.isEmpty()) {
344 blobName = QFileInfo(resolvedPath).baseName();
345 } else if (!isBareFileName(blobName)) {
346 // blob_name is later used verbatim as a file-name fallback by extract_ic
347 // (ICHandler::handleExtractIC) — reject path traversal here too, not just there,
348 // so a malicious name can't ride through the registry as a stored blob key.
349 return createErrorResponse("blob_name must not contain path separators or directory components",
350 requestId, JsonRpcError::InvalidParams);
351 }
352
353 auto *reg = scene->icRegistry();
354 if (reg->hasBlob(blobName)) {
355 return createErrorResponse(QString("Blob name collision: an embedded IC named '%1' already exists").arg(blobName),
356 requestId, JsonRpcError::IcError);
357 }
358
359 const int count = reg->embedICsByFile(ic->file(), fileBytes, blobName);
360
361 QJsonObject result;
362 result["blob_name"] = blobName;
363 result["converted_count"] = count;
364 result["input_count"] = ic->inputSize();
365 result["output_count"] = ic->outputSize();
366 result["blob_size"] = fileBytes.size();
367 result["message"] = QString("Embedded %1 IC(s) as '%2'").arg(count).arg(blobName);
368
369 return createSuccessResponse(result, requestId);
370 }, "embed IC", requestId);
371}
372
373QJsonObject ICHandler::handleExtractIC(const QJsonObject &params, const QJsonValue &requestId)
374{
375 if (!validateParameters(params, {"blob_name"})) {
376 return createErrorResponse("Missing required parameter: blob_name", requestId, JsonRpcError::InvalidParams);
377 }
378
379 Scene *scene = currentScene();
380 if (!scene) {
381 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
382 }
383
384 const QString contextDir = scene->contextDir();
385 if (contextDir.isEmpty()) {
386 return createErrorResponse("Project must be saved before extracting ICs", requestId, JsonRpcError::IcError);
387 }
388
389 const QString blobName = params.value("blob_name").toString();
390 if (blobName.isEmpty()) {
391 return createErrorResponse("blob_name must not be empty", requestId, JsonRpcError::InvalidParams);
392 }
393
394 return tryCommand([&]() -> QJsonObject {
395 auto *reg = scene->icRegistry();
396 if (!reg->hasBlob(blobName)) {
397 return createErrorResponse(QString("No embedded IC with blob name '%1' found").arg(blobName),
398 requestId, JsonRpcError::IcError);
399 }
400
401 QString fileName = params.value("file_name").toString();
402 if (fileName.isEmpty()) {
403 fileName = QDir(contextDir).absoluteFilePath(blobName + ".panda");
404 } else if (QFileInfo(fileName).isRelative()) {
405 fileName = QDir(contextDir).absoluteFilePath(fileName);
406 }
407 if (!fileName.endsWith(".panda")) {
408 fileName.append(".panda");
409 }
410
411 // file_name is an MCP-client-supplied identifier, not a GUI-picked path (unlike
412 // ICController::extractSelectedIC, which always routes through an interactive
413 // getSaveFileName dialog a human must approve) — an absolute file_name or one with
414 // ".." components would otherwise let the write above land anywhere on disk. Validate
415 // the final resolved path rather than the individual inputs that built it, so this
416 // can't be bypassed by some other future parameter combination; subdirectories of
417 // contextDir remain allowed, only escapes are rejected.
418 const QString cleanContextDir = QDir::cleanPath(QDir(contextDir).absolutePath());
419 const QString cleanFileName = QDir::cleanPath(fileName);
420 if (cleanFileName != cleanContextDir && !cleanFileName.startsWith(cleanContextDir + '/')) {
421 return createErrorResponse("file_name must resolve to a location inside the project directory",
422 requestId, JsonRpcError::InvalidParams);
423 }
424 fileName = cleanFileName;
425
426 if (QFile::exists(fileName) && !params.value("overwrite").toBool()) {
427 return createErrorResponse(QString("File '%1' already exists. Set overwrite=true to replace it.").arg(fileName),
428 requestId, JsonRpcError::FileError);
429 }
430
431 const int count = reg->extractToFile(blobName, fileName);
432
433 QJsonObject result;
434 result["blob_name"] = blobName;
435 result["file_name"] = fileName;
436 result["converted_count"] = count;
437 result["message"] = QString("Extracted %1 IC(s) to '%2'").arg(count).arg(fileName);
438
439 return createSuccessResponse(result, requestId);
440 }, "extract IC", requestId);
441}
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:79
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:47
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:352
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:359
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.