diff --git a/.cursor/rules/snacclib7-release-workflow.mdc b/.cursor/rules/snacclib7-release-workflow.mdc new file mode 100644 index 0000000..ae5b1b7 --- /dev/null +++ b/.cursor/rules/snacclib7-release-workflow.mdc @@ -0,0 +1,28 @@ +--- +description: esnacc version bumps, tags, and release workflow on feature branches +alwaysApply: true +--- + +# snacclib7 release workflow + +## No release-notes markdown files + +- **Do not** add or commit `release-notes-*.md` (or any markdown release-notes files). +- Document behavior in commit messages, Jira, or MR description instead. + +## Version (`version.h`) + +- **Once per feature branch:** when opening the branch, compare `version.h` on `main` and bump **one** patch (or agreed minor) — set `RELDATE` to the current date (`DD.MM.YYYY`). +- **Do not** bump again on the same branch while `main` is still behind that version. +- **Before opening the MR:** refresh `RELDATE` in `version.h` to the current date (no version number change unless `main` has shipped and a new release is intentional). + +## Git tags + +- **Do not** tag the feature branch for a release that is not yet on `main`. +- After **merge to `main`**, tag the release commit (e.g. `7.0/7.0.11` matching `version.h`). + +## Global pin (`global` repo) + +- **After** esnacc is merged to `main` (and tagged if applicable): update `libs/snacclib7` in `global` on a branch targeting `master`/`main`. +- Do **not** pin `global` to a pre-merge esnacc feature-branch tip for a version that is not on `main` yet. +- Rebuild `esnacc7.exe` into `global/buildtools/` from the merged `main` commit before or with the pin MR. diff --git a/AGENTS.md b/AGENTS.md index 86407b5..d662169 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,6 +111,16 @@ See [.cursor/rules/test-specification-first.mdc](.cursor/rules/test-specificatio - When writing tests, focus on specifying the API — not on getting a green suite. - **Only exception:** the user explicitly asks to change tests, change product code, or revise the spec. +## Version and tags (`version.h`) + +See [.cursor/rules/snacclib7-release-workflow.mdc](.cursor/rules/snacclib7-release-workflow.mdc). + +- Bump version **once** when opening a feature branch (compare to `main`); do not re-bump while `main` is still behind. +- Refresh `RELDATE` before opening the MR. +- Tag on **`main` after merge** (e.g. `7.0/7.0.11`); no release tags on the feature branch. +- Pin `global` **after** esnacc is on `main` (see release workflow rule). +- **No** `release-notes-*.md` files. + ## Related repositories - [esnacc-openapi-sdk](https://github.com/ESTOS/esnacc-openapi-sdk) — Swagger UI integration for generated OpenAPI output diff --git a/compiler/back-ends/structure-util.c b/compiler/back-ends/structure-util.c index 1d0082f..97a3762 100644 --- a/compiler/back-ends/structure-util.c +++ b/compiler/back-ends/structure-util.c @@ -578,6 +578,22 @@ bool IsDeprecatedFlaggedSequence(Module* mod, const char* szSequenceName) return false; } +bool IsIgnoreValidationExemptSequence(Module* mod, const char* szSequenceName, int validationCheck) +{ + asnsequencecomment comment; + if (GetSequenceComment_UTF8(mod->moduleName, szSequenceName, &comment)) + { + if (comment.iIgnoreValidation & validationCheck) + return true; + } + return false; +} + +bool IsValidationExemptSequence(Module* mod, const char* szSequenceName, int validationCheck) +{ + return IsDeprecatedFlaggedSequence(mod, szSequenceName) || IsIgnoreValidationExemptSequence(mod, szSequenceName, validationCheck); +} + bool IsDeprecatedNoOutputSequence(Module* mod, const char* szSequenceName) { if (!gi64NoDeprecatedSymbols) @@ -601,6 +617,22 @@ bool IsDeprecatedFlaggedOperation(Module* mod, const char* szOperationName) return false; } +bool IsIgnoreValidationExemptOperation(Module* mod, const char* szOperationName, int validationCheck) +{ + asnoperationcomment comment; + if (GetOperationComment_UTF8(mod->moduleName, szOperationName, &comment)) + { + if (comment.iIgnoreValidation & validationCheck) + return true; + } + return false; +} + +bool IsValidationExemptOperation(Module* mod, const char* szOperationName, int validationCheck) +{ + return IsDeprecatedFlaggedOperation(mod, szOperationName) || IsIgnoreValidationExemptOperation(mod, szOperationName, validationCheck); +} + bool IsDeprecatedNoOutputOperation(Module* mod, const char* szOperationName) { if (!gi64NoDeprecatedSymbols) diff --git a/compiler/back-ends/structure-util.h b/compiler/back-ends/structure-util.h index 3461c9a..e431d40 100644 --- a/compiler/back-ends/structure-util.h +++ b/compiler/back-ends/structure-util.h @@ -62,6 +62,14 @@ bool IsDeprecatedFlaggedMember(Module* mod, const TypeDef* td, const char* szEle bool IsDeprecatedFlaggedSequence(Module* mod, const char* szSequenceName); bool IsDeprecatedFlaggedOperation(Module* mod, const char* szOperationName); +// @ignorevalidation on SEQUENCE/OPERATION: skip selected ValidationLevel checks (bitmask). +bool IsIgnoreValidationExemptSequence(Module* mod, const char* szSequenceName, int validationCheck); +bool IsIgnoreValidationExemptOperation(Module* mod, const char* szOperationName, int validationCheck); + +// True when a type or operation is exempt from a ValidationLevel check (@deprecated or matching @ignorevalidation bit). +bool IsValidationExemptSequence(Module* mod, const char* szSequenceName, int validationCheck); +bool IsValidationExemptOperation(Module* mod, const char* szOperationName, int validationCheck); + // Returns true when an element is flagged as deprecated AND shall not be written to the output bool IsDeprecatedNoOutputModule(Module* mod); bool IsDeprecatedNoOutputMember(Module* mod, const TypeDef* td, const char* szElement); diff --git a/compiler/core/asn_commentparser.cpp b/compiler/core/asn_commentparser.cpp index c9264c4..5e78200 100644 --- a/compiler/core/asn_commentparser.cpp +++ b/compiler/core/asn_commentparser.cpp @@ -1,6 +1,7 @@ #include "asn_commentparser.h" #include "asn-stringconvert.h" #include "filetype.h" +#include "snacc-validation-rules.h" #include "../../snacc.h" #include "time_helpers.h" #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +19,21 @@ const std::string WHITESPACE = " \n\r\t\f\v"; +namespace +{ +constexpr std::string_view kTagBrief = "@brief"; +constexpr std::string_view kTagLong = "@long"; +constexpr std::string_view kTagPrivate = "@private"; +constexpr std::string_view kTagDeprecated = "@deprecated"; +constexpr std::string_view kTagAdded = "@added"; +constexpr std::string_view kTagIgnoreValidation = "@ignorevalidation"; +constexpr std::string_view kTagCategory = "@category"; +constexpr std::string_view kTagLogfilter = "@logfilter"; +constexpr std::string_view kTagLinked = "@linked"; +constexpr std::string_view kTagClear = "@clear"; +constexpr std::string_view kCommentIgnoredPrefix = "-- ~"; +} // namespace + bool isFiltered(const ETypeComment& comment) { if (!gPrivateSymbols && comment.iPrivate) @@ -72,6 +89,19 @@ std::string trim(const std::string& s) return rtrim(ltrim(s)); } +namespace +{ +[[nodiscard]] std::string remainderAfterTag(std::string_view line, std::string_view tag) +{ + return std::string(line.substr(tag.size())); +} + +[[nodiscard]] std::string trimmedRemainderAfterTag(std::string_view line, std::string_view tag) +{ + return trim(remainderAfterTag(line, tag)); +} +} // namespace + /** * Converts a unix time into something readable * @@ -239,24 +269,24 @@ void convertCommentList(std::list& commentList, ETypeComment* pType int nEmptyLines = 0; for (auto el = commentList.begin(); el != commentList.end(); el++) { - std::string strLine = *el; - if (strLine.substr(0, 6) == "@brief") + std::string strLine = trim(*el); + if (strLine.starts_with(kTagBrief)) { nEmptyLines = 0; bInLong = false; bInBrief = true; - strLine = trim(strLine.substr(6)); + strLine = trimmedRemainderAfterTag(strLine, kTagBrief); pType->strShort_UTF8 += escapeJsonString(strLine); } - else if (strLine.substr(0, 5) == "@long") + else if (strLine.starts_with(kTagLong)) { nEmptyLines = 0; bInBrief = false; bInLong = true; - strLine = trim(strLine.substr(5)); + strLine = trimmedRemainderAfterTag(strLine, kTagLong); pType->strLong_UTF8 += escapeJsonString(strLine); } - else if (strLine.substr(0, 8) == "@private") + else if (strLine.starts_with(kTagPrivate)) { nEmptyLines = 0; pType->iPrivate = 1; @@ -264,38 +294,51 @@ void convertCommentList(std::list& commentList, ETypeComment* pType // bInLong = false; // bInBrief = false; } - else if (strLine.substr(0, 11) == "@deprecated") + else if (strLine.starts_with(kTagDeprecated)) { nEmptyLines = 0; - pType->handleDeprecated(strLine.substr(11)); + pType->handleDeprecated(remainderAfterTag(strLine, kTagDeprecated)); // We do not change the flags here, the keyword @deprecated may lead or follow any comment // bInLong = false; // bInBrief = false; } - else if (strLine.substr(0, 6) == "@added") + else if (strLine.starts_with(kTagAdded)) { nEmptyLines = 0; - pType->handleAdded(strLine.substr(6)); + pType->handleAdded(remainderAfterTag(strLine, kTagAdded)); // We do not change the flags here, the keyword @deprecated may lead or follow any comment // bInLong = false; // bInBrief = false; } - else if (strLine.substr(0, 9) == "@category") + else if (strLine.starts_with(kTagIgnoreValidation)) { nEmptyLines = 0; - strLine = trim(strLine.substr(9)); + std::string strRules = trimmedRemainderAfterTag(strLine, kTagIgnoreValidation); + std::string strError; + const unsigned int nMask = ParseIgnoreValidationRulesSpec(strRules, &strError); + if (!nMask) + { + fprintf(stderr, "*** %s: %s ***\n", strRules.empty() ? "@ignorevalidation" : "@ignorevalidation rule error", strError.c_str()); + snacc_exit("Invalid @ignorevalidation tag."); + } + pType->m_nIgnoreValidationMask |= nMask; + } + else if (strLine.starts_with(kTagCategory)) + { + nEmptyLines = 0; + strLine = trimmedRemainderAfterTag(strLine, kTagCategory); // strLine += "\n"; pType->strCategory_UTF8 = escapeJsonString(strLine); bInLong = false; bInBrief = false; } - else if (strLine.substr(0, 10) == "@logfilter") + else if (strLine.starts_with(kTagLogfilter)) { nEmptyLines = 0; EModuleComment* pModuleComment = static_cast(pType); if (pModuleComment) { - strLine = trim(strLine.substr(10)); + strLine = trimmedRemainderAfterTag(strLine, kTagLogfilter); pModuleComment->strLogFilter = explode(strLine, ';'); } } @@ -360,39 +403,39 @@ void convertMemberCommentList(std::list& commentList, EStructMember for (auto el = commentList.begin(); el != commentList.end(); el++) { - std::string strLine = *el; - if (strLine.substr(0, 6) == "@brief") + std::string strLine = trim(*el); + if (strLine.starts_with(kTagBrief)) { last = eLast::_brief; - strLine = trim(strLine.substr(6)); + strLine = trimmedRemainderAfterTag(strLine, kTagBrief); pType->strShort_UTF8 += escapeJsonString(strLine); } - else if (strLine.substr(0, 8) == "@private") + else if (strLine.starts_with(kTagPrivate)) { last = eLast::_private; pType->iPrivate = 1; } - else if (strLine.substr(0, 11) == "@deprecated") + else if (strLine.starts_with(kTagDeprecated)) { last = eLast::_deprecated; - pType->handleDeprecated(strLine.substr(11)); + pType->handleDeprecated(remainderAfterTag(strLine, kTagDeprecated)); } - else if (strLine.substr(0, 6) == "@added") + else if (strLine.starts_with(kTagAdded)) { last = eLast::_added; - pType->handleAdded(strLine.substr(6)); + pType->handleAdded(remainderAfterTag(strLine, kTagAdded)); } - else if (strLine.substr(0, 7) == "@linked") + else if (strLine.starts_with(kTagLinked)) { last = eLast::_linked; - strLine = trim(strLine.substr(7)); + strLine = trimmedRemainderAfterTag(strLine, kTagLinked); pType->strLinkedType_UTF8 += escapeJsonString(strLine); } - else if (strLine.substr(0, 5) == "@long") + else if (strLine.starts_with(kTagLong)) { // in case someone added a long comment to a member variable we add the content to the short last = eLast::_brief; - strLine = trim(strLine.substr(5)); + strLine = trimmedRemainderAfterTag(strLine, kTagLong); if (!pType->strShort_UTF8.empty()) pType->strShort_UTF8 += escapeJsonString("\n"); pType->strShort_UTF8 += escapeJsonString(strLine); @@ -432,7 +475,7 @@ int EAsnStackElementFile::ProcessLine(const char* szModuleName, const char* szRa { if (!szComment.empty()) { - if (szComment.substr(0, 6) == "@clear") + if (szComment.starts_with(kTagClear)) m_CollectComments.clear(); else m_CollectComments.push_back(szComment); @@ -504,7 +547,7 @@ int EAsnStackElementModule::ProcessLine(const char* szModuleName, const char* sz { if (!szComment.empty()) { - if (szComment.substr(0, 6) == "@clear") + if (szComment.starts_with(kTagClear)) m_CollectComments.clear(); else m_CollectComments.push_back(szComment); @@ -1136,7 +1179,7 @@ void EAsnCommentParser::FilterFiles() auto strElements = explode(strFileContent, '\n', false, false); for (auto& strElement : strElements) { - if ((strElement.length() > 4 && strElement.substr(0, 5) == "-- ~ ") || (strElement.length() == 4 && strElement.substr(0, 4) == "-- ~")) + if (strElement.starts_with(kCommentIgnoredPrefix)) continue; strElement += "\n"; @@ -1171,7 +1214,7 @@ int EAsnCommentParser::ProcessLine(const char* szModuleName, const char* szLine) strLine = trim(strLine); // Comments only have the first leading space removed - if (strComment.substr(0, 1) == " ") + if (strComment.starts_with(' ')) strComment = strComment.substr(1, strComment.size() - 1); // strComment.TrimRight(); @@ -1180,7 +1223,7 @@ int EAsnCommentParser::ProcessLine(const char* szModuleName, const char* szLine) strComment = " "; // A comment starting with ~ is ignored - if (strComment.substr(0, 1) == "~") + if (strComment.starts_with('~')) strComment.clear(); } replaceAll(strLine, "\t", " "); @@ -1251,4 +1294,4 @@ long long EModuleComment::GetModulePatchVersion() } return m_i64ModuleVersion; -} \ No newline at end of file +} diff --git a/compiler/core/asn_commentparser.h b/compiler/core/asn_commentparser.h index ef2f886..761fe4d 100644 --- a/compiler/core/asn_commentparser.h +++ b/compiler/core/asn_commentparser.h @@ -64,6 +64,8 @@ class EStructMemberComment : public EDeprecated, public EAdded class ETypeComment : public EDeprecated, public EAdded { public: + virtual ~ETypeComment() = default; + // Name of the strCategory std::string strCategory_UTF8; std::string strCategory_ASCII; @@ -78,6 +80,8 @@ class ETypeComment : public EDeprecated, public EAdded std::string strLong_ASCII; // Type is private int iPrivate = 0; + // Bitmask of ValidationLevel checks to skip (see snacc-validation-rules.h). 0 = not set. + unsigned int m_nIgnoreValidationMask = 0; // Interal flag that stores whether the UTF8 value has already been converted to ascii (is done on access) bool m_bConvertedToAscii = false; }; diff --git a/compiler/core/asn_comments.cpp b/compiler/core/asn_comments.cpp index 5a250b5..40a93fe 100644 --- a/compiler/core/asn_comments.cpp +++ b/compiler/core/asn_comments.cpp @@ -164,6 +164,7 @@ extern "C" pcomment->i64Added = comment.i64Added; pcomment->i64Deprecated = comment.i64Deprecated; pcomment->szDeprecated = comment.strDeprecated_UTF8.c_str(); + pcomment->iIgnoreValidation = static_cast(comment.m_nIgnoreValidationMask); return 1; } return 0; @@ -199,6 +200,7 @@ extern "C" pcomment->i64Added = comment.i64Added; pcomment->i64Deprecated = comment.i64Deprecated; pcomment->szDeprecated = comment.strDeprecated_ASCII.c_str(); + pcomment->iIgnoreValidation = static_cast(comment.m_nIgnoreValidationMask); return 1; } return 0; @@ -226,6 +228,7 @@ extern "C" pcomment->i64Added = comment.i64Added; pcomment->i64Deprecated = comment.i64Deprecated; pcomment->szDeprecated = comment.strDeprecated_UTF8.c_str(); + pcomment->iIgnoreValidation = static_cast(comment.m_nIgnoreValidationMask); return 1; } return 0; @@ -261,6 +264,7 @@ extern "C" pcomment->i64Added = comment.i64Added; pcomment->i64Deprecated = comment.i64Deprecated; pcomment->szDeprecated = comment.strDeprecated_ASCII.c_str(); + pcomment->iIgnoreValidation = static_cast(comment.m_nIgnoreValidationMask); return 1; } return 0; diff --git a/compiler/core/asn_comments.h b/compiler/core/asn_comments.h index 09fb198..dc7e92a 100644 --- a/compiler/core/asn_comments.h +++ b/compiler/core/asn_comments.h @@ -31,6 +31,7 @@ extern "C" long long i64Added; long long i64Deprecated; const char* szDeprecated; + int iIgnoreValidation; /* bitmask; see snacc-validation-rules.h */ } asnoperationcomment; typedef struct _asnsequencecomment @@ -43,6 +44,7 @@ extern "C" long long i64Added; long long i64Deprecated; const char* szDeprecated; + int iIgnoreValidation; /* bitmask; see snacc-validation-rules.h */ } asnsequencecomment; typedef struct _asnmembercomment @@ -83,4 +85,4 @@ extern "C" } #endif -#endif \ No newline at end of file +#endif diff --git a/compiler/core/snacc-validation-rules.cpp b/compiler/core/snacc-validation-rules.cpp new file mode 100644 index 0000000..6b18de2 --- /dev/null +++ b/compiler/core/snacc-validation-rules.cpp @@ -0,0 +1,230 @@ +#include "snacc-validation-rules.h" + +#include "../../c-lib/include/asn-config.h" +#include "../../c-lib/include/platform-functions.h" +#include +#include +#include +#include + +namespace +{ + /** + * Case-insensitive ASCII compare for @ignorevalidation rule tokens. + * Uses mytolower (platform-functions) so MSVC and GCC/Clang builds share the same path. + */ + bool equalsIgnoreCase(const std::string& left, const char* pszRight) + { + const char* pszRightSafe = pszRight ? pszRight : ""; + std::string leftCopy(left); + std::string rightCopy(pszRightSafe); + if (!leftCopy.empty()) + mytolower(&leftCopy[0]); + if (!rightCopy.empty()) + mytolower(&rightCopy[0]); + return leftCopy == rightCopy; + } + + struct SValidationRuleAlias + { + const char* pszAlias; + EValidationCheck check; + }; + + static const SnaccValidationRuleDesc g_canonicalRules[] = { +#define X(id, bit, tag, desc) {SNACC_VAL_##id, bit, tag, desc}, + SNACC_VALIDATION_RULES_LIST(X) +#undef X + }; + + /* Legacy @ignorevalidation names kept for compatibility with earlier 7.0.12 drafts. */ + static const SValidationRuleAlias g_ruleAliases[] = { + {"duplicate-opid", SNACC_VAL_UNIQUE_OPERATION_ID}, + {"arg-shape", SNACC_VAL_ROSE_PAYLOAD_EXTENDABLE}, + {"error-type", SNACC_VAL_UNIFORM_OPERATION_ERROR}, + {"extendable", SNACC_VAL_SEQUENCE_HAS_ELLIPSIS}, + {"whitelist", SNACC_VAL_PRIMITIVE_TYPE_WHITELIST}, + {"rose-shape", SNACC_VAL_ROSE_INVOKE_EVENT_SHAPE}, + {"mixed-optionals", SNACC_VAL_NO_MIXED_OPTIONAL_ENCODING}, + {"mixed-optional", SNACC_VAL_NO_MIXED_OPTIONAL_ENCODING}, + {"explicit-optionals", SNACC_VAL_NO_UNTAGGED_OPTIONAL_MEMBERS}, + {"explicit-optional", SNACC_VAL_NO_UNTAGGED_OPTIONAL_MEMBERS}, + {"optional-params-bag", SNACC_VAL_NO_ASN_OPTIONAL_PARAMETERS}, + {"optional-params", SNACC_VAL_NO_ASN_OPTIONAL_PARAMETERS}, + {"optionalparams", SNACC_VAL_NO_ASN_OPTIONAL_PARAMETERS}, + }; + + std::string trimToken(const std::string& token) + { + size_t start = 0; + while (start < token.size() && std::isspace(static_cast(token[start]))) + ++start; + size_t end = token.size(); + while (end > start && std::isspace(static_cast(token[end - 1]))) + --end; + return token.substr(start, end - start); + } + + std::vector splitRuleTokens(const std::string& spec) + { + std::vector tokens; + std::string current; + for (char ch : spec) + { + if (ch == ',' || ch == ';' || std::isspace(static_cast(ch))) + { + if (!current.empty()) + { + tokens.push_back(trimToken(current)); + current.clear(); + } + } + else + current += ch; + } + if (!current.empty()) + tokens.push_back(trimToken(current)); + return tokens; + } + + bool lookupCanonicalRuleName(const std::string& token, unsigned int* pBit) + { + for (const SnaccValidationRuleDesc& rule : g_canonicalRules) + { + if (equalsIgnoreCase(token, rule.pszTagName)) + { + *pBit = rule.nBit; + return true; + } + } + return false; + } + + bool lookupRuleAlias(const std::string& token, unsigned int* pBit) + { + for (const SValidationRuleAlias& alias : g_ruleAliases) + { + if (equalsIgnoreCase(token, alias.pszAlias)) + { + *pBit = static_cast(alias.check); + return true; + } + } + return false; + } + + bool lookupRuleName(const std::string& token, unsigned int* pBit) + { + return lookupCanonicalRuleName(token, pBit) || lookupRuleAlias(token, pBit); + } + + unsigned int parseIgnoreValidationRulesSpecInternal(const std::string& spec, std::string* pError) + { + const std::string trimmed = trimToken(spec); + if (trimmed.empty()) + { + if (pError) + { + *pError = "@ignorevalidation requires at least one rule name " + "(e.g. no-mixed-optional-encoding, no-untagged-optional-members, no-asn-optional-parameters)."; + } + return 0; + } + + unsigned int mask = 0; + for (const std::string& token : splitRuleTokens(trimmed)) + { + unsigned int bit = 0; + if (lookupRuleName(token, &bit)) + { + mask |= bit; + continue; + } + + bool bAllDigits = !token.empty(); + for (char ch : token) + { + if (!std::isdigit(static_cast(ch))) + { + bAllDigits = false; + break; + } + } + if (bAllDigits) + { + mask |= static_cast(std::stoul(token)); + continue; + } + + if (pError) + { + *pError = "Unknown @ignorevalidation rule '"; + *pError += token; + *pError += "'."; + } + return 0; + } + + if (!mask && pError) + *pError = "@ignorevalidation rule list did not resolve to any validation bit."; + + return mask; + } +} // namespace + +size_t SnaccGetValidationRuleCount(void) +{ + return sizeof(g_canonicalRules) / sizeof(g_canonicalRules[0]); +} + +const SnaccValidationRuleDesc* SnaccGetValidationRule(size_t index) +{ + if (index >= SnaccGetValidationRuleCount()) + return NULL; + return &g_canonicalRules[index]; +} + +unsigned int ParseIgnoreValidationRulesSpec(const char* pszSpec, char* pszError, size_t cbError) +{ + std::string error; + const unsigned int mask = parseIgnoreValidationRulesSpecInternal(pszSpec ? pszSpec : "", &error); + if (!mask && pszError && cbError > 0 && !error.empty()) + strcpy_s(pszError, cbError, error.c_str()); + return mask; +} + +unsigned int ParseIgnoreValidationRulesSpec(const std::string& spec, std::string* pError) +{ + return parseIgnoreValidationRulesSpecInternal(spec, pError); +} + +void PrintValidationLevelHelp(FILE* fp) +{ + if (!fp) + return; + + fprintf(fp, " 0 no validation\n"); + for (size_t i = 0; i < SnaccGetValidationRuleCount(); ++i) + { + const SnaccValidationRuleDesc* rule = SnaccGetValidationRule(i); + if (!rule) + continue; + fprintf(fp, " %u %s\n", rule->nBit, rule->pszDescription); + } +} + +void PrintIgnoreValidationRuleNames(FILE* fp) +{ + if (!fp) + return; + + fprintf(fp, " @ignorevalidation rule names (comma/space separated; map to -ValidationLevel bits):\n "); + for (size_t i = 0; i < SnaccGetValidationRuleCount(); ++i) + { + const SnaccValidationRuleDesc* rule = SnaccGetValidationRule(i); + if (!rule) + continue; + fprintf(fp, "%s (%u)%s", rule->pszTagName, rule->nBit, (i + 1 < SnaccGetValidationRuleCount()) ? ", " : "\n"); + } + fprintf(fp, " Example: -- @ignorevalidation no-mixed-optional-encoding, no-untagged-optional-members, no-asn-optional-parameters\n"); +} diff --git a/compiler/core/snacc-validation-rules.h b/compiler/core/snacc-validation-rules.h new file mode 100644 index 0000000..6cfc84a --- /dev/null +++ b/compiler/core/snacc-validation-rules.h @@ -0,0 +1,81 @@ +#ifndef SNACC_VALIDATION_RULES_H +#define SNACC_VALIDATION_RULES_H + +#include +#include + +/* + * Single source of truth for esnacc -ValidationLevel bits and @ignorevalidation rule names. + * Included from C and C++ translation units. + * + * Extend SNACC_VALIDATION_RULES_LIST only — enum values, tag names, and help text follow from here. + */ +#define SNACC_VALIDATION_RULES_LIST(X) \ + X(UNIQUE_OPERATION_ID, 1, "unique-operation-id", \ + "Validates that operation IDs are unique within the module") \ + X(ROSE_PAYLOAD_EXTENDABLE, 2, "rose-payload-extendable", \ + "ROSE argument, result, and error types must be SEQUENCE or CHOICE") \ + X(UNIFORM_OPERATION_ERROR, 4, "uniform-operation-error", \ + "All ROSE operation ERROR types in a module must be the same type") \ + X(SEQUENCE_HAS_ELLIPSIS, 8, "sequence-has-ellipsis", \ + "SEQUENCE types must end with ... for extensibility") \ + X(PRIMITIVE_TYPE_WHITELIST, 16, "primitive-type-whitelist", \ + "Only ASN.1 primitive types listed in esnacc_whitelist.txt are allowed") \ + X(ROSE_INVOKE_EVENT_SHAPE, 32, "rose-invoke-event-shape", \ + "ROSE invoke operations need argument, result, and error; events only an argument") \ + X(NO_MIXED_OPTIONAL_ENCODING, 64, "no-mixed-optional-encoding", \ + "SEQUENCE must not mix context-tagged [n] OPTIONAL and untagged OPTIONAL members") \ + X(NO_UNTAGGED_OPTIONAL_MEMBERS, 128, "no-untagged-optional-members", \ + "OPTIONAL SEQUENCE members must use context tags [n], not untagged OPTIONAL") \ + X(NO_ASN_OPTIONAL_PARAMETERS, 256, "no-asn-optional-parameters", \ + "SEQUENCE must not declare optionalParams / AsnOptionalParameters members") + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef enum EValidationCheck + { +#define X(id, bit, tag, desc) SNACC_VAL_##id = bit, + SNACC_VALIDATION_RULES_LIST(X) +#undef X + } EValidationCheck; + + /* All per-type / per-operation checks (excludes unique-operation-id). */ +#define SNACC_VAL_ALL_TYPE_CHECKS 0x000001FE + + typedef struct SnaccValidationRuleDesc + { + EValidationCheck check; + unsigned int nBit; + const char* pszTagName; + const char* pszDescription; + } SnaccValidationRuleDesc; + + /* Canonical rules (one row per bit). */ + size_t SnaccGetValidationRuleCount(void); + const SnaccValidationRuleDesc* SnaccGetValidationRule(size_t index); + + /* Parse @ignorevalidation rule list (canonical names, legacy aliases, and/or numeric bits). */ + unsigned int ParseIgnoreValidationRulesSpec(const char* pszSpec, char* pszError, size_t cbError); + + void PrintValidationLevelHelp(FILE* fp); + void PrintIgnoreValidationRuleNames(FILE* fp); + +#ifdef __cplusplus +} + +enum class EValidationCheckClass : unsigned int +{ +#define X(id, bit, tag, desc) id = SNACC_VAL_##id, + SNACC_VALIDATION_RULES_LIST(X) +#undef X +}; + +#include + +unsigned int ParseIgnoreValidationRulesSpec(const std::string& spec, std::string* pError); +#endif + +#endif /* SNACC_VALIDATION_RULES_H */ diff --git a/compiler/core/snacc-validators.c b/compiler/core/snacc-validators.c index f25287a..fae6741 100644 --- a/compiler/core/snacc-validators.c +++ b/compiler/core/snacc-validators.c @@ -1,6 +1,7 @@ #include "snacc-validators.h" #include "../core/asn1module.h" #include "../core/print.h" +#include "../core/snacc-validation-rules.h" #include "snacc.h" #include "compiler/back-ends/structure-util.h" #include "mem.h" @@ -8,26 +9,6 @@ #include #include -enum EVALIDATIONCHECK -{ - // Do not allow duplicated operation ids - NO_DUPLICATE_OPERATIONIDS = 1, - // Check that arguments results and error are extendable objects (not e.g. lists which are not extendable) - OPERATION_ARGUMENT_RESULT_ERROR_ARE_CHOICES_OR_SEQUENCES = 2, - // Check that all errors are the same type to ensure generalized handling - OPERATION_ERRORS_ARE_OF_SAME_TYPE = 4, - // Check that all sequences have an extension attribute as last element (...) - SEQUENCES_ARE_EXTENDABLE = 8, - // Validate that the used Attribute types are mentioned in the esnacc_whitelist.txt (side by side with the asn1 files) - VALIDATE_TYPE_WHITELISTE = 16, - // Validate that invokes consist of an argument, result and error and events only of an error - VALIDATE_PROPER_INVOKE_EVENT_ARGUMENTS = 32, - // Validate that optionals are not encoded mixed (implicit and explicit in the same object) - VALIDATE_OPTIONALS_NO_MIXED_OPTIONALS = 64, - // Validate that optional are not encoded explicit (without number) - VALIDATE_OPTIONALS_NO_EXPLICIT_OPTIONALS = 128 -}; - // These methods return true on success (no error) or false on error // Reports multi use of operation ids @@ -246,41 +227,46 @@ const char* getTypeName(enum BasicTypeChoiceId choiceId) void ValidateASN1Data(ModuleList* allMods) { bool bSucceeded = true; - if (giValidationLevel & NO_DUPLICATE_OPERATIONIDS) + if (giValidationLevel & SNACC_VAL_UNIQUE_OPERATION_ID) { if (!ValidateNoDuplicateOperationIDs(allMods)) bSucceeded = false; } - if (giValidationLevel & OPERATION_ARGUMENT_RESULT_ERROR_ARE_CHOICES_OR_SEQUENCES) + if (giValidationLevel & SNACC_VAL_ROSE_PAYLOAD_EXTENDABLE) { if (!ValidateArgumentResultErrorAreSequencesOrChoices(allMods)) bSucceeded = false; } - if (giValidationLevel & OPERATION_ERRORS_ARE_OF_SAME_TYPE) + if (giValidationLevel & SNACC_VAL_UNIFORM_OPERATION_ERROR) { if (!ValidateErrorsAreOfSameType(allMods)) bSucceeded = false; } - if (giValidationLevel & SEQUENCES_ARE_EXTENDABLE) + if (giValidationLevel & SNACC_VAL_SEQUENCE_HAS_ELLIPSIS) { if (!ValidateSequencesAreExtendable(allMods)) bSucceeded = false; } - if (giValidationLevel & VALIDATE_TYPE_WHITELISTE) + if (giValidationLevel & SNACC_VAL_PRIMITIVE_TYPE_WHITELIST) { if (!ValidateOnlySupportedObjects(allMods)) bSucceeded = false; } - if (giValidationLevel & VALIDATE_PROPER_INVOKE_EVENT_ARGUMENTS) + if (giValidationLevel & SNACC_VAL_ROSE_INVOKE_EVENT_SHAPE) { if (!ValidateProperROSEArguments(allMods)) bSucceeded = false; } - if (giValidationLevel & (VALIDATE_OPTIONALS_NO_MIXED_OPTIONALS | VALIDATE_OPTIONALS_NO_EXPLICIT_OPTIONALS)) + if (giValidationLevel & (SNACC_VAL_NO_MIXED_OPTIONAL_ENCODING | SNACC_VAL_NO_UNTAGGED_OPTIONAL_MEMBERS)) { if (!ValidateOptionals(allMods)) bSucceeded = false; } + if (giValidationLevel & SNACC_VAL_NO_ASN_OPTIONAL_PARAMETERS) + { + if (!ValidateNoOptionalParamsBag(allMods)) + bSucceeded = false; + } if (!bSucceeded) snacc_exit("Validation failed. Terminating..."); } @@ -357,7 +343,7 @@ bool ValidateArgumentResultErrorAreSequencesOrChoices(ModuleList* allMods) if (!IsROSEValueDef(currMod, vd)) continue; - if (IsDeprecatedFlaggedOperation(currMod, vd->definedName)) + if (IsValidationExemptOperation(currMod, vd->definedName, SNACC_VAL_ROSE_PAYLOAD_EXTENDABLE)) continue; const char* pszArgument = NULL; @@ -474,7 +460,7 @@ bool ValidateErrorsAreOfSameType(ModuleList* allMods) if (!IsROSEValueDef(currMod, vd)) continue; - if (IsDeprecatedFlaggedOperation(currMod, vd->definedName)) + if (IsValidationExemptOperation(currMod, vd->definedName, SNACC_VAL_UNIFORM_OPERATION_ERROR)) continue; const char* pszError = NULL; @@ -558,7 +544,7 @@ bool ValidateSequencesAreExtendable(ModuleList* allMods) TypeDef* td; FOR_EACH_LIST_ELMT(td, mod->typeDefs) { - if (IsDeprecatedFlaggedSequence(mod, td->definedName)) + if (IsValidationExemptSequence(mod, td->definedName, SNACC_VAL_SEQUENCE_HAS_ELLIPSIS)) continue; struct BasicType* type = td->type->basicType; @@ -642,9 +628,9 @@ bool recurseFindInvalid(Module* mod, Type* type, int* supportedTypes, const char if (szElementName) { - if (choiceId == BASICTYPE_SEQUENCE && IsDeprecatedFlaggedSequence(mod, szElementName)) + if (choiceId == BASICTYPE_SEQUENCE && IsValidationExemptSequence(mod, szElementName, SNACC_VAL_PRIMITIVE_TYPE_WHITELIST)) return false; - else if (strstr(szPath, "::") == NULL && IsDeprecatedFlaggedSequence(mod, szElementName)) + else if (strstr(szPath, "::") == NULL && IsValidationExemptSequence(mod, szElementName, SNACC_VAL_PRIMITIVE_TYPE_WHITELIST)) return false; } @@ -674,14 +660,14 @@ bool recurseFindInvalid(Module* mod, Type* type, int* supportedTypes, const char if (szElementName) { - if (choiceId == BASICTYPE_SEQUENCE && IsDeprecatedFlaggedSequence(mod, szElementName)) + if (choiceId == BASICTYPE_SEQUENCE && IsValidationExemptSequence(mod, szElementName, SNACC_VAL_PRIMITIVE_TYPE_WHITELIST)) return false; char szNewName[TESTBUFFERSIZE + 1] = {0}; strcat_s(szNewName, TESTBUFFERSIZE, "::"); strcat_s(szNewName, TESTBUFFERSIZE, szElementName); if ((choiceId == BASICTYPE_SEQUENCE || choiceId == BASICTYPE_LOCALTYPEREF || choiceId == BASICTYPE_IMPORTTYPEREF) && type->cxxTypeRefInfo->className) { - if (IsDeprecatedFlaggedSequence(mod, type->cxxTypeRefInfo->className)) + if (IsValidationExemptSequence(mod, type->cxxTypeRefInfo->className, SNACC_VAL_PRIMITIVE_TYPE_WHITELIST)) return false; strcat_s(szNewName, TESTBUFFERSIZE, "("); strcat_s(szNewName, TESTBUFFERSIZE, type->cxxTypeRefInfo->className); @@ -938,7 +924,7 @@ bool ValidateProperROSEArguments(ModuleList* allMods) if (!IsROSEValueDef(currMod, vd)) continue; - if (IsDeprecatedFlaggedOperation(currMod, vd->definedName)) + if (IsValidationExemptOperation(currMod, vd->definedName, SNACC_VAL_ROSE_INVOKE_EVENT_SHAPE)) continue; const char* pszArgument = NULL; @@ -996,7 +982,9 @@ bool ValidateOptionals(ModuleList* allMods) TypeDef* td; FOR_EACH_LIST_ELMT(td, mod->typeDefs) { - if (IsDeprecatedFlaggedSequence(mod, td->definedName)) + const bool bExemptMixed = IsValidationExemptSequence(mod, td->definedName, SNACC_VAL_NO_MIXED_OPTIONAL_ENCODING); + const bool bExemptExplicit = IsValidationExemptSequence(mod, td->definedName, SNACC_VAL_NO_UNTAGGED_OPTIONAL_MEMBERS); + if (bExemptMixed && bExemptExplicit) continue; struct BasicType* type = td->type->basicType; @@ -1059,17 +1047,17 @@ bool ValidateOptionals(ModuleList* allMods) int nError = 0; // Validate that optionals are not encoded mixed (implicit and explicit in the same object) - if (giValidationLevel & VALIDATE_OPTIONALS_NO_MIXED_OPTIONALS) + if ((giValidationLevel & SNACC_VAL_NO_MIXED_OPTIONAL_ENCODING) && !bExemptMixed) { if (bNotContextSpecific_Explicit_Optional & bContextSpecific_Implicit_Optional) - nError |= VALIDATE_OPTIONALS_NO_MIXED_OPTIONALS; + nError |= SNACC_VAL_NO_MIXED_OPTIONAL_ENCODING; } // Validate that optional are not encoded explicit (without number) - if (giValidationLevel & VALIDATE_OPTIONALS_NO_EXPLICIT_OPTIONALS) + if ((giValidationLevel & SNACC_VAL_NO_UNTAGGED_OPTIONAL_MEMBERS) && !bExemptExplicit) { if (bNotContextSpecific_Explicit_Optional) - nError |= VALIDATE_OPTIONALS_NO_EXPLICIT_OPTIONALS; + nError |= SNACC_VAL_NO_UNTAGGED_OPTIONAL_MEMBERS; } if (!nError) @@ -1087,9 +1075,9 @@ bool ValidateOptionals(ModuleList* allMods) iErrorCounter = 0; } - if (nError & VALIDATE_OPTIONALS_NO_MIXED_OPTIONALS) + if (nError & SNACC_VAL_NO_MIXED_OPTIONAL_ENCODING) fprintf(stderr, "- %s contains explicit (%s) and implicit (%s) optionals, use only implicit (with number)\n", szFirstExplicitOptional, szFirstImplicitOptional, td->definedName); - else if (nError & VALIDATE_OPTIONALS_NO_EXPLICIT_OPTIONALS) + else if (nError & SNACC_VAL_NO_UNTAGGED_OPTIONAL_MEMBERS) fprintf(stderr, "- %s contains explicit optionals, use only implicit (with number)\n", td->definedName); iErrorCounter++; @@ -1098,6 +1086,94 @@ bool ValidateOptionals(ModuleList* allMods) } } + if (iErrorCounter) + fprintf(stderr, " -> File contained %i error(s)\n\n", iErrorCounter); + + return nWeHaveErrors ? false : true; +} + +static bool MemberTypeIsAsnOptionalParameters(Type* memberType) +{ + if (!memberType || !memberType->basicType) + return false; + + if (memberType->cxxTypeRefInfo && memberType->cxxTypeRefInfo->className) + { + if (strcmp(memberType->cxxTypeRefInfo->className, "AsnOptionalParameters") == 0) + return true; + } + + BasicType* resolvedType = ResolveBasicTypeReferences(memberType->basicType, NULL); + if (!resolvedType) + return false; + + if (resolvedType->choiceId == BASICTYPE_LOCALTYPEREF || resolvedType->choiceId == BASICTYPE_IMPORTTYPEREF) + { + if (memberType->cxxTypeRefInfo && memberType->cxxTypeRefInfo->className) + return strcmp(memberType->cxxTypeRefInfo->className, "AsnOptionalParameters") == 0; + } + + return false; +} + +bool ValidateNoOptionalParamsBag(ModuleList* allMods) +{ + int nWeHaveErrors = 0; + int iErrorCounter = 0; + const char* szLastErrorFile = NULL; + Module* mod; + FOR_EACH_LIST_ELMT(mod, allMods) + { + if (mod->ImportedFlag == FALSE) + { + TypeDef* td; + FOR_EACH_LIST_ELMT(td, mod->typeDefs) + { + if (IsValidationExemptSequence(mod, td->definedName, SNACC_VAL_NO_ASN_OPTIONAL_PARAMETERS)) + continue; + + struct BasicType* type = td->type->basicType; + if (!type || type->choiceId != BASICTYPE_SEQUENCE) + continue; + + NamedType* subType; + bool bHasOptionalParamsBag = false; + FOR_EACH_LIST_ELMT(subType, type->a.sequence) + { + if (subType->type->basicType->choiceId == BASICTYPE_EXTENSION) + continue; + if (MemberTypeIsAsnOptionalParameters(subType->type)) + { + bHasOptionalParamsBag = true; + break; + } + } + + if (!bHasOptionalParamsBag) + continue; + + if (!nWeHaveErrors) + fprintf(stderr, "*** Validating that SEQUENCE types do not use legacy AsnOptionalParameters... ***\n"); + + if (szLastErrorFile != mod->asn1SrcFileName) + { + if (szLastErrorFile) + fprintf(stderr, " -> File contained %i error(s)\n\n", iErrorCounter); + fprintf(stderr, "Errors in %s:\n", mod->asn1SrcFileName); + szLastErrorFile = mod->asn1SrcFileName; + iErrorCounter = 0; + } + + fprintf(stderr, + "- %s uses legacy AsnOptionalParameters (optionalParams bag); use dedicated [n] OPTIONAL fields instead " + "(@ignorevalidation no-asn-optional-parameters only for grandfathered types)\n", + td->definedName); + nWeHaveErrors++; + iErrorCounter++; + } + } + } + if (iErrorCounter) fprintf(stderr, " -> File contained %i error(s)\n\n", iErrorCounter); diff --git a/compiler/core/snacc-validators.h b/compiler/core/snacc-validators.h index 2a981e3..2ae4e14 100644 --- a/compiler/core/snacc-validators.h +++ b/compiler/core/snacc-validators.h @@ -3,3 +3,4 @@ #include "asn1module.h" void ValidateASN1Data(ModuleList* allMods); +bool ValidateNoOptionalParamsBag(ModuleList* allMods); diff --git a/compiler/core/snacc.c b/compiler/core/snacc.c index 53ab199..6bba95b 100644 --- a/compiler/core/snacc.c +++ b/compiler/core/snacc.c @@ -53,6 +53,7 @@ char* bVDAGlobalDLLExport = (char*)0; #include "define.h" #include "snacc-util.h" #include "snacc-validators.h" +#include "snacc-validation-rules.h" #include "filetype.h" #include "../back-ends/structure-util.h" #include "../back-ends/str-util.h" @@ -261,15 +262,9 @@ void Usage PARAMS((prgName, fp), char* prgName _AND_ FILE* fp) fprintf(fp, " -nodeprecated do not generate code that is marked as deprecated (any date)\n"); fprintf(fp, " -nodeprecated:Day.Month.Year do not generate code that has been marked deprecated prior to this date\n"); fprintf(fp, " -ValidationLevel n - Sets a specific validation rule set for the asn1 files. Default is that all of the following checks are applied\n"); - fprintf(fp, " 0 no validation\n"); - fprintf(fp, " 1 Validates that operationIDs are not used twice\n"); - fprintf(fp, " 2 Validates that operation arguments, results and errors are sequences or choices (only types are extendable) (@deprecated are not validated)\n"); - fprintf(fp, " 4 Validates that errors are of the same type to generalize error handling (@deprecated are not validated)\n"); - fprintf(fp, " 8 Validates that all sequences contain ... to allow extending them (@deprecated are not validated)\n"); - fprintf(fp, " 16 Validates that only allow types from the esnacc_whiteliste.txt are used (@deprecated are not validated)\n"); - fprintf(fp, " 32 Ensure that invokes are specified with argument, result and error where events only consists of an argument (@deprecated are not validated)\n"); - fprintf(fp, " 64 Ensure that optional parameters are not encoded implicit (context specific, with number) and explicit (without) in the same object (@deprecated are not validated)\n"); - fprintf(fp, " 128 Ensure that optional parameters are always encoded implicit and never explicit (@deprecated are not validated)\n"); + fprintf(fp, " @deprecated exempts a type/operation from all checks below. @ignorevalidation exempts only the named rules (see below).\n"); + PrintValidationLevelHelp(fp); + PrintIgnoreValidationRuleNames(fp); fprintf(fp, " -versionfile - the compiler writes a version file for the highest version found (requires interfaceversion.txt)\n"); fprintf(fp, " -utf8 write output files with UTF-8 encoding (default: system codepage / Windows-1252)\n"); fprintf(fp, " -utf8bom write a UTF-8 BOM at the start of each output file (implies -utf8)\n"); diff --git a/samples/interface/ENetUC_Common.asn1 b/samples/interface/ENetUC_Common.asn1 index 7be0d70..c7fdc09 100644 --- a/samples/interface/ENetUC_Common.asn1 +++ b/samples/interface/ENetUC_Common.asn1 @@ -114,6 +114,7 @@ AsnSystemTime ::= REAL -- AsnNetDatabaseContact is widly used almost everywhere in the API. It contains the contact data of a contact and from which databse it comes from. -- All fields in this sequence are optional, because not every field must have content, but the sequence is broadly used, so every field which is -- not transmitted saves bandwidth. +-- @ignorevalidation no-asn-optional-parameters AsnNetDatabaseContact ::= SEQUENCE { u8sFound [0] UTF8String OPTIONAL, diff --git a/snacc.h b/snacc.h index ab22abe..27bf3be 100644 --- a/snacc.h +++ b/snacc.h @@ -8,7 +8,14 @@ #define NULL 0 #endif +#ifdef __cplusplus +extern "C" +{ +#endif void snacc_exit_now(const char* szMethod, const char* szMessage, ...); +#ifdef __cplusplus +} +#endif #define snacc_exit(szMessage, ...) \ { \ snacc_exit_now(__func__, szMessage, ##__VA_ARGS__); \ diff --git a/version.h b/version.h index df883d1..ee8dd12 100644 --- a/version.h +++ b/version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define VERSION "7.0.10" -#define VERSION_RC 7, 0, 10 -#define RELDATE "24.07.2026" +#define VERSION "7.0.11" +#define VERSION_RC 7, 0, 11 +#define RELDATE "06.08.2026" #endif // VERSION_H