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