This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin.git


The following commit(s) were added to refs/heads/master by this push:
     new 0dd2ea7  fix: keep ", " separator in SavePolicy output
0dd2ea7 is described below

commit 0dd2ea76ef0c7007b3fe22ce3c6905799bac0f4f
Author: Yang Luo <[email protected]>
AuthorDate: Tue Aug 4 16:08:42 2026 +0800

    fix: keep ", " separator in SavePolicy output
---
 persist/adapter.go      | 25 +++++++++----
 persist/persist_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 116 insertions(+), 7 deletions(-)

diff --git a/persist/adapter.go b/persist/adapter.go
index a259e44..8178bde 100644
--- a/persist/adapter.go
+++ b/persist/adapter.go
@@ -45,20 +45,31 @@ func LoadPolicyLine(line string, m model.Model) error {
 }
 
 // PolicyLineToCsv serializes a policy type and its rule fields into a 
CSV-safe line.
+// Fields are separated by ", " to match the conventional layout of Casbin 
policy
+// files, which LoadPolicyLine accepts because its reader trims leading spaces.
 // Fields containing commas are properly quoted so the line can be 
round-tripped
 // through LoadPolicyLine without corruption.
 func PolicyLineToCsv(ptype string, rule []string) (string, error) {
        record := append([]string{ptype}, rule...)
+
+       // Each field is written as its own single-field record so that the 
standard
+       // library decides on quoting, then the fields are joined with ", ".
        var buf bytes.Buffer
        w := csv.NewWriter(&buf)
-       if err := w.Write(record); err != nil {
-               return "", err
-       }
-       w.Flush()
-       if err := w.Error(); err != nil {
-               return "", err
+       fields := make([]string, 0, len(record))
+       for _, field := range record {
+               buf.Reset()
+               if err := w.Write([]string{field}); err != nil {
+                       return "", err
+               }
+               w.Flush()
+               if err := w.Error(); err != nil {
+                       return "", err
+               }
+               fields = append(fields, strings.TrimRight(buf.String(), "\r\n"))
        }
-       return strings.TrimRight(buf.String(), "\r\n"), nil
+
+       return strings.Join(fields, ", "), nil
 }
 
 // LoadPolicyArray loads a policy rule to model.
diff --git a/persist/persist_test.go b/persist/persist_test.go
index f5face3..1a2adb8 100644
--- a/persist/persist_test.go
+++ b/persist/persist_test.go
@@ -18,6 +18,7 @@
 package persist_test
 
 import (
+       "reflect"
        "testing"
 
        "github.com/casbin/casbin/v3"
@@ -54,3 +55,100 @@ func TestDuplicateRuleInAdapter(t *testing.T) {
 
        testRuleCount(t, e.GetModel(), 1, "p", "p", "LoadPolicyArray")
 }
+
+func TestPolicyLineToCsv(t *testing.T) {
+       tests := []struct {
+               name     string
+               ptype    string
+               rule     []string
+               expected string
+       }{
+               {
+                       // Fields are separated by ", ", the layout used by the 
policy files
+                       // under examples/, so that SavePolicy does not 
reformat them.
+                       name:     "plain fields keep the space after the comma",
+                       ptype:    "p",
+                       rule:     []string{"alice", "data1", "read"},
+                       expected: "p, alice, data1, read",
+               },
+               {
+                       name:     "field containing a comma is quoted",
+                       ptype:    "p",
+                       rule:     []string{"alice", "data1", "read", "r.attrs 
in ('val1','val2')"},
+                       expected: `p, alice, data1, read, "r.attrs in 
('val1','val2')"`,
+               },
+               {
+                       name:     "field containing a quote is escaped",
+                       ptype:    "p",
+                       rule:     []string{"alice", `say "hi"`, "read"},
+                       expected: `p, alice, "say ""hi""", read`,
+               },
+               {
+                       // A leading space must stay quoted: LoadPolicyLine 
trims leading
+                       // spaces on unquoted fields.
+                       name:     "field with a leading space is quoted",
+                       ptype:    "g",
+                       rule:     []string{" alice", "admin"},
+                       expected: `g, " alice", admin`,
+               },
+               {
+                       name:     "empty field",
+                       ptype:    "p",
+                       rule:     []string{"alice", "", "read"},
+                       expected: "p, alice, , read",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       line, err := persist.PolicyLineToCsv(tt.ptype, tt.rule)
+                       if err != nil {
+                               t.Fatalf("PolicyLineToCsv: %v", err)
+                       }
+                       if line != tt.expected {
+                               t.Errorf("line: %q, expected %q", line, 
tt.expected)
+                       }
+               })
+       }
+}
+
+func TestPolicyLineToCsvRoundTrip(t *testing.T) {
+       conf := `
+[request_definition]
+r = sub, obj, act, cond
+
+[policy_definition]
+p = sub, obj, act, cond
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
+`
+       rule := []string{" alice", `say "hi"`, "read", "r.attrs in 
('val1','val2')"}
+
+       line, err := persist.PolicyLineToCsv("p", rule)
+       if err != nil {
+               t.Fatalf("PolicyLineToCsv: %v", err)
+       }
+
+       m := model.NewModel()
+       if err := m.LoadModelFromText(conf); err != nil {
+               t.Fatalf("load model: %v", err)
+       }
+       if err := persist.LoadPolicyLine(line, m); err != nil {
+               t.Fatalf("LoadPolicyLine on %q: %v", line, err)
+       }
+
+       rules, err := m.GetPolicy("p", "p")
+       if err != nil {
+               t.Fatalf("GetPolicy: %v", err)
+       }
+       if len(rules) != 1 {
+               t.Fatalf("rule count: %d, expected 1 (line: %q)", len(rules), 
line)
+       }
+       if !reflect.DeepEqual(rules[0], rule) {
+               t.Errorf("rule after round-trip: %q, expected %q (line: %q)", 
rules[0], rule, line)
+       }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to