| @@ -17,6 +17,12 @@ void setError(std::string *error, const std::string &message) | |||
| } | |||
| } | |||
| bool containsLineBreak(const std::string &value) | |||
| { | |||
| return value.find('\r') != std::string::npos | |||
| || value.find('\n') != std::string::npos; | |||
| } | |||
| bool validateConfig(const ContactNodeConfig &config, std::string *error) | |||
| { | |||
| if (!config.address.isValid() || config.address.area() != RegisterArea::M) | |||
| @@ -831,10 +837,19 @@ bool LadderRung::validateStructure(std::string *error) const | |||
| setError(error, "梯形图网络 ID 不能超过 128 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes | |||
| || comment.size() > ProjectLimits::kMaximumTextBytes) | |||
| if (name.size() > ProjectLimits::kMaximumTextBytes) | |||
| { | |||
| setError(error, "梯形图网络名称不能超过 4096 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (containsLineBreak(comment)) | |||
| { | |||
| setError(error, "梯形图网络注释只能使用单行文本"); | |||
| return false; | |||
| } | |||
| if (comment.size() > ProjectLimits::kMaximumRungCommentBytes) | |||
| { | |||
| setError(error, "梯形图网络名称和注释不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "梯形图网络注释不能超过 128 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| std::vector<std::string> node_ids; | |||
| @@ -27,6 +27,8 @@ constexpr std::size_t kMaximumIdBytes = 128U; | |||
| constexpr std::size_t kMaximumTextBytes = 4096U; | |||
| constexpr std::size_t kMaximumPropertyKeyBytes = 128U; | |||
| constexpr std::size_t kMaximumPropertyValueBytes = 4096U; | |||
| constexpr std::size_t kMaximumRegisterCommentBytes = 64U; | |||
| constexpr std::size_t kMaximumRungCommentBytes = 128U; | |||
| constexpr int kMaximumHmiPageWidth = 8192; | |||
| constexpr int kMaximumHmiPageHeight = 8192; | |||
| @@ -64,6 +64,12 @@ bool isBlank(const std::string &value) | |||
| [](unsigned char character) { return std::isspace(character) != 0; }); | |||
| } | |||
| bool containsLineBreak(const std::string &value) | |||
| { | |||
| return value.find('\r') != std::string::npos | |||
| || value.find('\n') != std::string::npos; | |||
| } | |||
| } // namespace | |||
| bool RegisterComment::validate(std::string *error) const | |||
| @@ -78,9 +84,14 @@ bool RegisterComment::validate(std::string *error) const | |||
| setError(error, "软元件注释内容不能为空"); | |||
| return false; | |||
| } | |||
| if (text.size() > ProjectLimits::kMaximumTextBytes) | |||
| if (containsLineBreak(text)) | |||
| { | |||
| setError(error, "软元件注释只能使用单行文本"); | |||
| return false; | |||
| } | |||
| if (text.size() > ProjectLimits::kMaximumRegisterCommentBytes) | |||
| { | |||
| setError(error, "软元件注释不能超过 4096 个 UTF-8 字节"); | |||
| setError(error, "软元件注释不能超过 64 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| return true; | |||
| @@ -21,6 +21,12 @@ bool isBlank(const std::string &value) | |||
| [](unsigned char character) { return std::isspace(character) != 0; }); | |||
| } | |||
| bool containsLineBreak(const std::string &value) | |||
| { | |||
| return value.find('\r') != std::string::npos | |||
| || value.find('\n') != std::string::npos; | |||
| } | |||
| std::string makeUniqueLogicId(const Project &project) | |||
| { | |||
| int suffix = 1; | |||
| @@ -865,11 +871,17 @@ LogicEditorResult LogicEditorService::updateRungComment( | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); | |||
| } | |||
| if (comment.size() > ProjectLimits::kMaximumTextBytes) | |||
| if (containsLineBreak(comment)) | |||
| { | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "梯形图网络注释只能使用单行文本"); | |||
| } | |||
| if (comment.size() > ProjectLimits::kMaximumRungCommentBytes) | |||
| { | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "梯形图网络注释不能超过 4096 个 UTF-8 字节"); | |||
| "梯形图网络注释不能超过 128 个 UTF-8 字节"); | |||
| } | |||
| if (rung->comment == comment) | |||
| { | |||
| @@ -79,7 +79,7 @@ void drawRegisterComment( | |||
| comment, Qt::ElideRight, static_cast<int>(kCellWidth - 8.0)); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, top, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| Qt::AlignCenter | Qt::TextSingleLine, | |||
| visible_comment); | |||
| } | |||
| @@ -810,7 +810,7 @@ public: | |||
| comment_, Qt::ElideRight, static_cast<int>(width_ - 16.0)); | |||
| painter->drawText( | |||
| QRectF(kSceneMargin + 8, 24, width_ - 16, 22), | |||
| Qt::AlignLeft | Qt::AlignVCenter, | |||
| Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, | |||
| visible_comment); | |||
| } | |||
| painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); | |||
| @@ -1339,10 +1339,11 @@ void MainWindow::editSelectedRungComment() | |||
| return; | |||
| } | |||
| bool accepted = false; | |||
| const QString comment = QInputDialog::getMultiLineText( | |||
| const QString comment = QInputDialog::getText( | |||
| this, | |||
| tr("网络注释"), | |||
| tr("说明"), | |||
| QLineEdit::Normal, | |||
| fromUtf8(rung->comment), | |||
| &accepted); | |||
| if (!accepted) | |||
| @@ -497,9 +497,29 @@ void testTimerAndCommentBoundaries() | |||
| RegisterComment comment{RegisterAddress{RegisterArea::M, 0}, "启动按钮"}; | |||
| require(comment.validate(), "a nonblank register comment must be valid"); | |||
| comment.text.assign(ProjectLimits::kMaximumRegisterCommentBytes, 'a'); | |||
| require(comment.validate(), "a register comment at the byte limit must be valid"); | |||
| comment.text.push_back('a'); | |||
| require(!comment.validate(), "a register comment above the byte limit must fail"); | |||
| comment.text = "启动\n按钮"; | |||
| require(!comment.validate(), "a multiline register comment must be rejected"); | |||
| comment.text = "启动\r按钮"; | |||
| require(!comment.validate(), "a register comment containing CR must be rejected"); | |||
| comment.text = " \t"; | |||
| require(!comment.validate(), "a blank register comment must be rejected"); | |||
| LadderRung comment_rung; | |||
| comment_rung.id = "comment-rung"; | |||
| comment_rung.name = "Comment rung"; | |||
| comment_rung.comment.assign(ProjectLimits::kMaximumRungCommentBytes, 'a'); | |||
| require(comment_rung.validate(), "a rung comment at the byte limit must be valid"); | |||
| comment_rung.comment.push_back('a'); | |||
| require(!comment_rung.validate(), "a rung comment above the byte limit must fail"); | |||
| comment_rung.comment = "第一行\n第二行"; | |||
| require(!comment_rung.validate(), "a multiline rung comment must be rejected"); | |||
| comment_rung.comment = "第一行\r第二行"; | |||
| require(!comment_rung.validate(), "a rung comment containing CR must be rejected"); | |||
| Project project = makeValidProject(); | |||
| project.registerComments = { | |||
| {RegisterAddress{RegisterArea::M, 0}, "启动按钮"}, | |||
| @@ -1,4 +1,5 @@ | |||
| #include "domain/project_storage.h" | |||
| #include "domain/project_limits.h" | |||
| #include "services/logic_editor_service.h" | |||
| #include "services/project_service.h" | |||
| @@ -712,6 +713,15 @@ void testEdgeTimerNodesAndRungComments() | |||
| "the editor must apply TON preset properties"); | |||
| require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded, | |||
| "the editor must update a network comment by stable rung id"); | |||
| require(!service.updateRungComment( | |||
| logic_id, rung_id, "第一行\n第二行").succeeded, | |||
| "the editor must reject multiline network comments"); | |||
| require(!service.updateRungComment( | |||
| logic_id, | |||
| rung_id, | |||
| std::string(ProjectLimits::kMaximumRungCommentBytes + 1U, 'a')) | |||
| .succeeded, | |||
| "the editor must reject oversized network comments"); | |||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | |||
| require(rung != nullptr && rung->comment == "延时启动网络" | |||
| @@ -435,6 +435,10 @@ void testExampleProjectRoundTrip() | |||
| "missing-register-comments.json"); | |||
| const QString missing_rung_comment_path = directory.filePath( | |||
| "missing-rung-comment.json"); | |||
| const QString multiline_register_comment_path = directory.filePath( | |||
| "multiline-register-comment.json"); | |||
| const QString multiline_rung_comment_path = directory.filePath( | |||
| "multiline-rung-comment.json"); | |||
| require(service.saveAs(first_path.toStdString()).succeeded, | |||
| "example project save must succeed"); | |||
| const QByteArray saved_json = readBytes(first_path); | |||
| @@ -671,6 +675,41 @@ void testExampleProjectRoundTrip() | |||
| && missing_result.storageError == ProjectStorageError::MissingField, | |||
| "the 1.0 schema must require rung comments without migration defaults"); | |||
| QJsonObject multiline_register_comment = QJsonDocument::fromJson( | |||
| saved_json).object(); | |||
| QJsonArray comments = multiline_register_comment.value( | |||
| QStringLiteral("registerComments")).toArray(); | |||
| QJsonObject first_comment = comments.at(0).toObject(); | |||
| first_comment.insert(QStringLiteral("text"), QStringLiteral("第一行\n第二行")); | |||
| comments.replace(0, first_comment); | |||
| multiline_register_comment.insert(QStringLiteral("registerComments"), comments); | |||
| writeText( | |||
| multiline_register_comment_path, | |||
| QJsonDocument(multiline_register_comment).toJson(QJsonDocument::Compact)); | |||
| missing_result = service.load(multiline_register_comment_path.toStdString()); | |||
| require(!missing_result.succeeded | |||
| && missing_result.storageError == ProjectStorageError::InvalidProject, | |||
| "JSON loading must reject multiline register comments"); | |||
| QJsonObject multiline_rung_comment = QJsonDocument::fromJson(saved_json).object(); | |||
| QJsonArray multiline_logics = multiline_rung_comment.value( | |||
| QStringLiteral("controlLogics")).toArray(); | |||
| first_logic = multiline_logics.at(0).toObject(); | |||
| first_rungs = first_logic.value(QStringLiteral("rungs")).toArray(); | |||
| first_rung = first_rungs.at(0).toObject(); | |||
| first_rung.insert(QStringLiteral("comment"), QStringLiteral("第一行\n第二行")); | |||
| first_rungs.replace(0, first_rung); | |||
| first_logic.insert(QStringLiteral("rungs"), first_rungs); | |||
| multiline_logics.replace(0, first_logic); | |||
| multiline_rung_comment.insert(QStringLiteral("controlLogics"), multiline_logics); | |||
| writeText( | |||
| multiline_rung_comment_path, | |||
| QJsonDocument(multiline_rung_comment).toJson(QJsonDocument::Compact)); | |||
| missing_result = service.load(multiline_rung_comment_path.toStdString()); | |||
| require(!missing_result.succeeded | |||
| && missing_result.storageError == ProjectStorageError::InvalidProject, | |||
| "JSON loading must reject multiline rung comments"); | |||
| QJsonObject missing_target = QJsonDocument::fromJson(saved_json).object(); | |||
| QJsonArray pages = missing_target.value(QStringLiteral("hmiPages")).toArray(); | |||
| QJsonObject main_page = pages.at(0).toObject(); | |||
| @@ -887,6 +926,19 @@ void testRegisterCommentService() | |||
| "setting an existing address must update instead of duplicating it"); | |||
| require(!service.setComment(RegisterAddress{RegisterArea::M, 4}, " \t").succeeded, | |||
| "blank register comments must be rejected by the service"); | |||
| require(service.setComment( | |||
| RegisterAddress{RegisterArea::M, 4}, | |||
| std::string(ProjectLimits::kMaximumRegisterCommentBytes, 'a')) | |||
| .succeeded, | |||
| "a register comment at the byte limit must be accepted"); | |||
| require(!service.setComment( | |||
| RegisterAddress{RegisterArea::M, 5}, | |||
| std::string(ProjectLimits::kMaximumRegisterCommentBytes + 1U, 'a')) | |||
| .succeeded, | |||
| "an oversized register comment must be rejected by the service"); | |||
| require(!service.setComment( | |||
| RegisterAddress{RegisterArea::M, 5}, "第一行\n第二行").succeeded, | |||
| "a multiline register comment must be rejected by the service"); | |||
| require(service.removeComment(RegisterAddress{RegisterArea::M, 2}).succeeded | |||
| && service.findComment(RegisterAddress{RegisterArea::M, 2}) == nullptr, | |||
| "register comments must be removable"); | |||