Skip to content

Commit

Permalink
disambiguate empty sets from empty maps
Browse files Browse the repository at this point in the history
according to the grammar, `{}` could either mean *empty set* or *empty map*. Due to how the parser was written, it was always an empty set, and there was no way to have an empty map literal.

This is an issue because empty maps would still be displayed as `{}`, and maps have a different api than sets, resulting in evaluation errors.

This commit introduces `{,}` as the empty set literal, while `{}` is the empty map. The goal here is to be consistent with JSON, the reason why we chose the current syntax for arrays and maps.
  • Loading branch information
divarvel committed Nov 29, 2024
1 parent fc0d069 commit b852ee0
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 12 deletions.
3 changes: 3 additions & 0 deletions biscuit-auth/examples/testcases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,9 @@ fn expressions(target: &str, root: &KeyPair, test: bool) -> TestResult {
check if {1, 2, 3}.intersection({1, 2}).contains(1);
// chained method calls with unary method
check if {1, 2, 3}.intersection({1, 2}).length() === 2;
// empty set literal
check if {,}.length() === 0;
"#)
.build_with_rng(&root, SymbolTable::default(), &mut rng)
.unwrap();
Expand Down
4 changes: 3 additions & 1 deletion biscuit-auth/samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,7 @@ check if {1, 2}.intersection({2, 3}) === {2};
check if {1, 2}.union({2, 3}) === {1, 2, 3};
check if {1, 2, 3}.intersection({1, 2}).contains(1);
check if {1, 2, 3}.intersection({1, 2}).length() === 2;
check if {,}.length() === 0;
```

### validation
Expand All @@ -1331,7 +1332,7 @@ allow if true;
```

revocation ids:
- `d0420227266e3583a42dfaa0e38550d99f681d150dd18856f3af9a697bc9c5c8bf06b4b0fe5b9df0377d1b963574e2fd210a0a76a8b0756a65f640c602bebd07`
- `fa358e4e3bea896415b1859e6cd347e64e1918fb86e31ae3fe208628321576a47f7a269760357e291c827ec9cbe322074f6860a546207a64e133c83a214bb505`

authorizer world:
```
Expand Down Expand Up @@ -1372,6 +1373,7 @@ World {
"check if true",
"check if true === true",
"check if {\"abc\", \"def\"}.contains(\"abc\")",
"check if {,}.length() === 0",
"check if {1, 2, 3}.intersection({1, 2}).contains(1)",
"check if {1, 2, 3}.intersection({1, 2}).length() === 2",
"check if {1, 2} === {1, 2}",
Expand Down
5 changes: 3 additions & 2 deletions biscuit-auth/samples/samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -1276,7 +1276,7 @@
],
"public_keys": [],
"external_key": null,
"code": "check if true;\ncheck if !false;\ncheck if true === true;\ncheck if false === false;\ncheck if 1 < 2;\ncheck if 2 > 1;\ncheck if 1 <= 2;\ncheck if 1 <= 1;\ncheck if 2 >= 1;\ncheck if 2 >= 2;\ncheck if 3 === 3;\ncheck if 1 + 2 * 3 - 4 / 2 === 5;\ncheck if \"hello world\".starts_with(\"hello\"), \"hello world\".ends_with(\"world\");\ncheck if \"aaabde\".matches(\"a*c?.e\");\ncheck if \"aaabde\".contains(\"abd\");\ncheck if \"aaabde\" === \"aaa\" + \"b\" + \"de\";\ncheck if \"abcD12\" === \"abcD12\";\ncheck if \"abcD12\".length() === 6;\ncheck if \"é\".length() === 2;\ncheck if 2019-12-04T09:46:41Z < 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z > 2019-12-04T09:46:41Z;\ncheck if 2019-12-04T09:46:41Z <= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2019-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z === 2020-12-04T09:46:41Z;\ncheck if hex:12ab === hex:12ab;\ncheck if {1, 2}.contains(2);\ncheck if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z);\ncheck if {false, true}.contains(true);\ncheck if {\"abc\", \"def\"}.contains(\"abc\");\ncheck if {hex:12ab, hex:34de}.contains(hex:34de);\ncheck if {1, 2}.contains({2});\ncheck if {1, 2} === {1, 2};\ncheck if {1, 2}.intersection({2, 3}) === {2};\ncheck if {1, 2}.union({2, 3}) === {1, 2, 3};\ncheck if {1, 2, 3}.intersection({1, 2}).contains(1);\ncheck if {1, 2, 3}.intersection({1, 2}).length() === 2;\n",
"code": "check if true;\ncheck if !false;\ncheck if true === true;\ncheck if false === false;\ncheck if 1 < 2;\ncheck if 2 > 1;\ncheck if 1 <= 2;\ncheck if 1 <= 1;\ncheck if 2 >= 1;\ncheck if 2 >= 2;\ncheck if 3 === 3;\ncheck if 1 + 2 * 3 - 4 / 2 === 5;\ncheck if \"hello world\".starts_with(\"hello\"), \"hello world\".ends_with(\"world\");\ncheck if \"aaabde\".matches(\"a*c?.e\");\ncheck if \"aaabde\".contains(\"abd\");\ncheck if \"aaabde\" === \"aaa\" + \"b\" + \"de\";\ncheck if \"abcD12\" === \"abcD12\";\ncheck if \"abcD12\".length() === 6;\ncheck if \"é\".length() === 2;\ncheck if 2019-12-04T09:46:41Z < 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z > 2019-12-04T09:46:41Z;\ncheck if 2019-12-04T09:46:41Z <= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2019-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z === 2020-12-04T09:46:41Z;\ncheck if hex:12ab === hex:12ab;\ncheck if {1, 2}.contains(2);\ncheck if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z);\ncheck if {false, true}.contains(true);\ncheck if {\"abc\", \"def\"}.contains(\"abc\");\ncheck if {hex:12ab, hex:34de}.contains(hex:34de);\ncheck if {1, 2}.contains({2});\ncheck if {1, 2} === {1, 2};\ncheck if {1, 2}.intersection({2, 3}) === {2};\ncheck if {1, 2}.union({2, 3}) === {1, 2, 3};\ncheck if {1, 2, 3}.intersection({1, 2}).contains(1);\ncheck if {1, 2, 3}.intersection({1, 2}).length() === 2;\ncheck if {,}.length() === 0;\n",
"version": 3
}
],
Expand Down Expand Up @@ -1317,6 +1317,7 @@
"check if true",
"check if true === true",
"check if {\"abc\", \"def\"}.contains(\"abc\")",
"check if {,}.length() === 0",
"check if {1, 2, 3}.intersection({1, 2}).contains(1)",
"check if {1, 2, 3}.intersection({1, 2}).length() === 2",
"check if {1, 2} === {1, 2}",
Expand All @@ -1339,7 +1340,7 @@
},
"authorizer_code": "allow if true;\n",
"revocation_ids": [
"d0420227266e3583a42dfaa0e38550d99f681d150dd18856f3af9a697bc9c5c8bf06b4b0fe5b9df0377d1b963574e2fd210a0a76a8b0756a65f640c602bebd07"
"fa358e4e3bea896415b1859e6cd347e64e1918fb86e31ae3fe208628321576a47f7a269760357e291c827ec9cbe322074f6860a546207a64e133c83a214bb505"
]
}
}
Expand Down
Binary file modified biscuit-auth/samples/test017_expressions.bc
Binary file not shown.
14 changes: 9 additions & 5 deletions biscuit-auth/src/datalog/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,15 @@ impl SymbolTable {
}
}
Term::Set(s) => {
let terms = s
.iter()
.map(|term| self.print_term(term))
.collect::<Vec<_>>();
format!("{{{}}}", terms.join(", "))
if s.is_empty() {
"{,}".to_string()
} else {
let terms = s
.iter()
.map(|term| self.print_term(term))
.collect::<Vec<_>>();
format!("{{{}}}", terms.join(", "))
}
}
Term::Null => "null".to_string(),
Term::Array(a) => {
Expand Down
4 changes: 4 additions & 0 deletions biscuit-auth/src/token/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,8 @@ check if true trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc
})
);
}
#[test]
fn empty_set_display() {
assert_eq!(Term::Set(BTreeSet::new()).to_string(), "{,}");
}
}
8 changes: 6 additions & 2 deletions biscuit-auth/src/token/builder/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,12 @@ impl fmt::Display for Term {
}
}
Term::Set(s) => {
let terms = s.iter().map(|term| term.to_string()).collect::<Vec<_>>();
write!(f, "{{{}}}", terms.join(", "))
if s.is_empty() {
write!(f, "{{,}}")
} else {
let terms = s.iter().map(|term| term.to_string()).collect::<Vec<_>>();
write!(f, "{{{}}}", terms.join(", "))
}
}
Term::Parameter(s) => {
write!(f, "{{{}}}", s)
Expand Down
26 changes: 24 additions & 2 deletions biscuit-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,8 +864,16 @@ fn null(i: &str) -> IResult<&str, builder::Term, Error> {
}

fn set(i: &str) -> IResult<&str, builder::Term, Error> {
alt((empty_set, non_empty_set))(i)
}

fn empty_set(i: &str) -> IResult<&str, builder::Term, Error> {
tag("{,}")(i).map(|(i, _)| (i, builder::set(BTreeSet::new())))
}

fn non_empty_set(i: &str) -> IResult<&str, builder::Term, Error> {
let (i, _) = preceded(space0, char('{'))(i)?;
let (i, mut list) = cut(separated_list0(preceded(space0, char(',')), term_in_set))(i)?;
let (i, mut list) = cut(separated_list1(preceded(space0, char(',')), term_in_set))(i)?;

let mut set = BTreeSet::new();

Expand Down Expand Up @@ -960,7 +968,7 @@ fn term(i: &str) -> IResult<&str, builder::Term, Error> {
preceded(
space0,
alt((
parameter, string, date, variable, integer, bytes, boolean, null, set, array, parse_map,
parameter, string, date, variable, integer, bytes, boolean, null, array, parse_map, set,
)),
)(i)
}
Expand Down Expand Up @@ -2659,4 +2667,18 @@ mod tests {
))
);
}

#[test]
fn empty_set_map() {
use builder::{map, set};

assert_eq!(
super::expr("{,}").map(|(i, o)| (i, o.opcodes())),
Ok(("", vec![Op::Value(set(Default::default()))],))
);
assert_eq!(
super::expr("{}").map(|(i, o)| (i, o.opcodes())),
Ok(("", vec![Op::Value(map(Default::default()))],))
);
}
}

0 comments on commit b852ee0

Please sign in to comment.