Skip to content

Commit 4c73883

Browse files
authored
feat: add BracketAwareCsvParser to parse CSV file better (#494)
* fix: improve policy line parsing This change makes the policy parser more robust and flexible, allowing for * chore: remove csv-parse dependency from package.json * refactor: use const instead of let for tokens array in Helper class * fix: policy line parsing for nested expressions and quoted values * feat: enhance csv-parse for policy * refactor: reorganize policy parsing and loading logic
1 parent ca66e06 commit 4c73883

File tree

5 files changed

+91
-15
lines changed

5 files changed

+91
-15
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@casbin/expression-eval": "^5.3.0",
5353
"await-lock": "^2.0.1",
5454
"buffer": "^6.0.3",
55-
"csv-parse": "^5.3.5",
55+
"csv-parse": "^5.5.6",
5656
"minimatch": "^7.4.2"
5757
},
5858
"files": [

src/persist/fileAdapter.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export class FileAdapter implements Adapter {
3434
private async loadPolicyFile(model: Model, handler: (line: string, model: Model) => void): Promise<void> {
3535
const bodyBuf = await (this.fs ? this.fs : mustGetDefaultFileSystem()).readFileSync(this.filePath);
3636
const lines = bodyBuf.toString().split('\n');
37-
lines.forEach((n: string, index: number) => {
38-
if (!n) {
37+
lines.forEach((line: string) => {
38+
if (!line || line.trim().startsWith('#')) {
3939
return;
4040
}
41-
handler(n, model);
41+
handler(line, model);
4242
});
4343
}
4444

src/persist/helper.ts

+82-6
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,83 @@
11
import { Model } from '../model';
22
import { parse } from 'csv-parse/sync';
33

4-
export class Helper {
5-
public static loadPolicyLine(line: string, model: Model): void {
4+
export interface IPolicyParser {
5+
parse(line: string): string[][] | null;
6+
}
7+
8+
export class BasicCsvParser implements IPolicyParser {
9+
parse(line: string): string[][] | null {
610
if (!line || line.trimStart().charAt(0) === '#') {
7-
return;
11+
return null;
812
}
913

10-
const tokens = parse(line, {
14+
return parse(line, {
1115
delimiter: ',',
1216
skip_empty_lines: true,
1317
trim: true,
18+
relax_quotes: true,
1419
});
20+
}
21+
}
22+
23+
export class BracketAwareCsvParser implements IPolicyParser {
24+
private readonly baseParser: IPolicyParser;
25+
26+
constructor(baseParser: IPolicyParser = new BasicCsvParser()) {
27+
this.baseParser = baseParser;
28+
}
29+
30+
parse(line: string): string[][] | null {
31+
const rawTokens = this.baseParser.parse(line);
32+
if (!rawTokens || !rawTokens[0]) {
33+
return null;
34+
}
35+
36+
const tokens = rawTokens[0];
37+
const processedTokens: string[] = [];
38+
let currentToken = '';
39+
let bracketCount = 0;
40+
41+
for (const token of tokens) {
42+
for (const char of token) {
43+
if (char === '(') bracketCount++;
44+
else if (char === ')') bracketCount--;
45+
}
46+
47+
currentToken += (currentToken ? ',' : '') + token;
48+
49+
if (bracketCount === 0) {
50+
processedTokens.push(currentToken);
51+
currentToken = '';
52+
}
53+
}
54+
55+
if (bracketCount !== 0) {
56+
throw new Error(`Unmatched brackets in policy line: ${line}`);
57+
}
58+
59+
return processedTokens.length > 0 ? [processedTokens] : null;
60+
}
61+
}
62+
63+
export class PolicyLoader {
64+
private readonly parser: IPolicyParser;
1565

66+
constructor(parser: IPolicyParser = new BracketAwareCsvParser()) {
67+
this.parser = parser;
68+
}
69+
70+
loadPolicyLine(line: string, model: Model): void {
71+
const tokens = this.parser.parse(line);
1672
if (!tokens || !tokens[0]) {
1773
return;
1874
}
1975

20-
const key = tokens[0][0];
76+
let key = tokens[0][0].trim();
77+
if (key.startsWith('"') && key.endsWith('"')) {
78+
key = key.slice(1, -1);
79+
}
80+
2181
const sec = key.substring(0, 1);
2282
const item = model.model.get(sec);
2383
if (!item) {
@@ -28,6 +88,22 @@ export class Helper {
2888
if (!policy) {
2989
return;
3090
}
31-
policy.policy.push(tokens[0].slice(1));
91+
92+
const values = tokens[0].slice(1).map((v) => {
93+
if (v.startsWith('"') && v.endsWith('"')) {
94+
v = v.slice(1, -1);
95+
}
96+
return v.replace(/""/g, '"').trim();
97+
});
98+
99+
policy.policy.push(values);
100+
}
101+
}
102+
103+
export class Helper {
104+
private static readonly policyLoader = new PolicyLoader();
105+
106+
public static loadPolicyLine(line: string, model: Model): void {
107+
Helper.policyLoader.loadPolicyLine(line, model);
32108
}
33109
}

test/persist/helper.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
4545
['admin', '/', 'POST'],
4646
['admin', '/', 'PUT'],
4747
['admin', '/', 'DELETE'],
48-
[' admin', '/ ', 'PATCH'],
48+
['admin', '/', 'PATCH'],
4949
];
5050

5151
testdata.forEach((n) => {

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -2282,10 +2282,10 @@ cssstyle@^2.3.0:
22822282
dependencies:
22832283
cssom "~0.3.6"
22842284

2285-
csv-parse@^5.3.5:
2286-
version "5.3.5"
2287-
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.3.5.tgz#9924bbba9f7056122f06b7af18edc1a7f022ce99"
2288-
integrity sha512-8O5KTIRtwmtD3+EVfW6BCgbwZqJbhTYsQZry12F1TP5RUp0sD9tp1UnCWic3n0mLOhzeocYaCZNYxOGSg3dmmQ==
2285+
csv-parse@^5.5.6:
2286+
version "5.5.6"
2287+
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.6.tgz#0d726d58a60416361358eec291a9f93abe0b6b1a"
2288+
integrity sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==
22892289

22902290
22912291
version "3.2.0"

0 commit comments

Comments
 (0)