`EscapeAssertion` rewrites dots inside quoted string literals

已关闭 适合新手
#276 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
1/5
预计耗时
1-3 小时
新手友好度
88/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
cpp
领域
tooling

调研方向

从 casbin/util/escape_assertion.cpp 中的 EscapeAssertion 开始,将其与 include/casbin/util/util.h 中的声明进行比较。将带引号的字面量用例添加到 tests/util_test.cpp 中的 TestEscapeAssertion,然后运行该测试二进制文件以查看当前失败。完成意味着单引号和双引号字面量保留其点号,而断言的 r/p 前缀仍会被重写。

由索引模型根据 Issue 内容生成。

描述

Location (apache/casbin-cpp @ default branch 0ad64a7):

std::string EscapeAssertion(std::string s) {
    std::regex regex_s("[a-zA-Z0-9. ]+");

    std::sregex_iterator words_begin = std::sregex_iterator(s.begin(), s.end(), regex_s);
    auto words_end = std::sregex_iterator();

    for (std::sregex_iterator k = words_begin; k != words_end; ++k) {
        std::smatch match = *k;
        std::string match_str = match.str();
        int pos = static_cast<int>(match_str.find("."));
        if (pos != -1) {
            std::string new_str = match_str.replace(pos, 1, "_");
            s = s.replace(match.position(), match.str().length(), new_str);
        }
    }

    return s;
}

Problem
casbin::EscapeAssertion replaces the first . in every [a-zA-Z0-9. ]+ run, including runs inside quoted string literals. The regex doesn't know about quotes: because " and ' are not in the class, the text between the quotes becomes its own match, and its first dot gets replaced.

input result expected
r.sub == "r.my_resource" r_sub == "r_my_resource" r_sub == "r.my_resource"
p.obj == 'p.resource' p_obj == 'p_resource' p_obj == 'p.resource'
p.sub == p.obj && p.obj == "r.something" p_sub == p_obj && p_obj == "r_something" p_sub == p_obj && p_obj == "r.something"
p.obj == "r.my_resource" p_obj == "r_my_resource" p_obj == "r.my_resource"

Scope: EscapeAssertion is declared in the public header include/casbin/util/util.h (included by casbin/casbin.h), documented as taking "the value of the matcher and effect assertions". In the current Enforce path it is only applied to the argument names of eval(...) (casbin/enforcer.cpp:120-124), so this is not an enforcement failure: Enforce results are not affected. This is about the result of the public utility for code that calls it directly (model tooling, converters).

Reproduction

#include <iostream>
#include <casbin/casbin.h>

int main() {
    std::cout << casbin::EscapeAssertion("r.sub == \"r.my_resource\"") << "\n";
    std::cout << casbin::EscapeAssertion("p.obj == 'p.resource'") << "\n";
}

The "result" column above is verbatim gtest output, e.g.:

Expected equality of these values:
  result
    Which is: "r_sub == \"r_my_resource\""
  "r_sub == \"r.my_resource\""

Environment:

  • Commit: 0ad64a7 (master)
  • Toolchain: GCC 11.5.0

Already fixed in apache/casbin-node-casbin
PR: https://github.com/apache/casbin-node-casbin/pull/514 — "feat: fix escapeAssertion to preserve string literals" (merged 2025-11-11T17:50:46Z, merge commit 9abab0fb8b5). node-casbin's escapeAssertion also rewrote r./p. inside string literals ("r.my_resource" became "r_my_resource"). It now only rewrites an r/p (optionally numbered) prefix that is preceded by start-of-string, whitespace, a parenthesis or an operator character; since quotes are not in that set, text inside literals is left alone. In node-casbin the helper is on the enforcement path, so the bug there also changed Enforce results; in casbin-cpp only the public helper's output is affected.

How node-casbin fixed it:

--- a/src/util/util.ts
+++ b/src/util/util.ts
@@ -17,11 +17,16 @@
 
 import { mustGetDefaultFileSystem } from '../persist';
 
-const escapeAssertionReg = new RegExp(/(^|[^A-Za-z0-9_])([rp])[0-9]*\./g);
+const escapeAssertionReg = new RegExp(/([()\s|&,=!><+\-*/]|^)((r|p)[0-9]*)\./g);
 
 function escapeAssertion(s: string): string {
-  s = s.replace(escapeAssertionReg, (match, p1, p2) => {
-    return p1 + p2 + match.substring(p1.length + p2.length).replace('.', '_');
+  s = s.replace(escapeAssertionReg, (match) => {
+    // Replace only the last dot with underscore (preserve the prefix character)
+    const lastDotIdx = match.lastIndexOf('.');
+    if (lastDotIdx > 0) {
+      return match.substring(0, lastDotIdx) + '_';
+    }
+    return match;
   });
   return s;
 }
--- a/test/util.test.ts
+++ b/test/util.test.ts
@@ -228,3 +228,27 @@ test('bracketCompatible', () => {
...
+test('test escapeAssertion', () => {
+  expect(util.escapeAssertion('r_sub == r_obj.value')).toEqual('r_sub == r_obj.value');
+  expect(util.escapeAssertion('p_sub == r_sub.value')).toEqual('p_sub == r_sub.value');
+  expect(util.escapeAssertion('r.attr.value == p.attr')).toEqual('r_attr.value == p_attr');
...
+  // Test that patterns inside strings are not escaped
+  expect(util.escapeAssertion('r.sub == "a.p.p.l.e"')).toEqual('r_sub == "a.p.p.l.e"');
+  expect(util.escapeAssertion('r.sub == "test.p.value"')).toEqual('r_sub == "test.p.value"');
+});

Suggested fix
Only rewrite an r/p token (optionally numbered) that follows start-of-string, whitespace, a parenthesis, a comma or an operator, as node-casbin now does, so dots inside single- or double-quoted literals are left alone:

diff --git a/casbin/util/escape_assertion.cpp b/casbin/util/escape_assertion.cpp
--- a/casbin/util/escape_assertion.cpp
+++ b/casbin/util/escape_assertion.cpp
@@ -32,22 +32,10 @@ namespace casbin {
  * @return the escaped value.
  */
 std::string EscapeAssertion(std::string s) {
-    std::regex regex_s("[a-zA-Z0-9. ]+");
-
-    std::sregex_iterator words_begin = std::sregex_iterator(s.begin(), s.end(), regex_s);
-    auto words_end = std::sregex_iterator();
-
-    for (std::sregex_iterator k = words_begin; k != words_end; ++k) {
-        std::smatch match = *k;
-        std::string match_str = match.str();
-        int pos = static_cast<int>(match_str.find("."));
-        if (pos != -1) {
-            std::string new_str = match_str.replace(pos, 1, "_");
-            s = s.replace(match.position(), match.str().length(), new_str);
-        }
-    }
-
-    return s;
+    // Only an r/p token (e.g. r., p., r2.) that follows start-of-string, whitespace, a parenthesis, a comma or an
+    // operator is escaped, so dots inside quoted string literals are left alone.
+    static const std::regex regex_s("([()\\s|&,=!><+\\-*/]|^)([rp][0-9]*)\\.");
+    return std::regex_replace(s, regex_s, "$1$2_");
 }
 
 } // namespace casbin

Verified on 0ad64a7 (GCC 11.4.0, GoogleTest 1.14.0): with the first three rows of the table above added to TestEscapeAssertion in tests/util_test.cpp, the test fails without this patch and passes with it; no other test results change, including the existing TestEscapeAssertion cases.

I can submit a PR for this.

Related: searched issues/PRs (all states) for EscapeAssertion, escape + string literal, eval rule + dot: no hits. The underscore-in-identifier case has a different cause and is reported separately.

主要语言
C++
星标
257
派生
67
平均合并
10 小时 23 分钟
30 天内合并 PR
1

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

apache/casbin-cpp 的其他 Issue

查看 apache/casbin-cpp 的全部 Issue

相似的 Issue

更多 C++ Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。