Skip to content

Commit 3a8b721

Browse files
committed
fix(golang): address the second review of the operator exports
- The catalog reader and se_query_term's reader share one parser each for an operator's name, fidelity and index. A name is one of eq, lt, lte, gt, gte, between and matches, the set the engine's OpSpec::parse accepts. The catalog took any non-empty name, so "like" or "Eq" parsed and failed later as unknown_operator from Cipher.QueryTerm, and se_query_term took any operator and any index, which the catalog refused. Both now refuse the same values, and a test feeds every name the engine writes through both. - The query fixture test checks that query_operators.json was recorded under queryPlan, so a fixture regenerated under another plan fails by name rather than as mismatched term bytes.
1 parent cb73591 commit 3a8b721

7 files changed

Lines changed: 133 additions & 37 deletions

File tree

‎languages/golang/encrypt/checker.go‎

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,9 @@ func targetOperators(value any) ([]record.TargetOperator, error) {
212212
}
213213
switch f.Key {
214214
case "operator":
215-
op.Name = s
215+
op.Name, err = parseOperatorName(s)
216216
case "fidelity":
217-
op.Fidelity = record.Fidelity(s)
218-
if op.Fidelity != record.Exact && op.Fidelity != record.Probabilistic {
219-
return nil, fmt.Errorf("operator %d: unknown fidelity %q", i, s)
220-
}
217+
op.Fidelity, err = parseFidelity(s)
221218
case "arity":
222219
switch s {
223220
case "one":
@@ -227,15 +224,13 @@ func targetOperators(value any) ([]record.TargetOperator, error) {
227224
return nil, fmt.Errorf("operator %d: unknown arity %q", i, s)
228225
}
229226
case "index":
230-
switch o := record.Output(s); o {
231-
case record.Equality, record.Match, record.Ore, record.Ope:
232-
op.Index = o
233-
default:
234-
return nil, fmt.Errorf("operator %d: unknown index %q", i, s)
235-
}
227+
op.Index, err = parseOperatorIndex(s)
236228
default:
237229
return nil, fmt.Errorf("operator %d: unknown key %q", i, f.Key)
238230
}
231+
if err != nil {
232+
return nil, fmt.Errorf("operator %d: %v", i, err)
233+
}
239234
}
240235
if len(seen) != 4 || op.Name == "" {
241236
return nil, fmt.Errorf("operator %d: not every key, or no name", i)
@@ -245,6 +240,38 @@ func targetOperators(value any) ([]record.TargetOperator, error) {
245240
return operators, nil
246241
}
247242

243+
// parseOperatorName reads an operator's name as the engine writes it: one
244+
// of eq, lt, lte, gt, gte, between and matches, the names the engine's
245+
// OpSpec::parse accepts. The catalog (se_targets) and se_query_term are
246+
// read through this, parseFidelity and parseOperatorIndex, so the two
247+
// readers refuse the same values.
248+
func parseOperatorName(s string) (string, error) {
249+
switch s {
250+
case "eq", "lt", "lte", "gt", "gte", "between", "matches":
251+
return s, nil
252+
}
253+
return "", fmt.Errorf("unknown operator %q", s)
254+
}
255+
256+
// parseFidelity reads an operator's fidelity: exact or probabilistic.
257+
func parseFidelity(s string) (record.Fidelity, error) {
258+
switch f := record.Fidelity(s); f {
259+
case record.Exact, record.Probabilistic:
260+
return f, nil
261+
}
262+
return "", fmt.Errorf("unknown fidelity %q", s)
263+
}
264+
265+
// parseOperatorIndex reads the index that answers an operator: eq, match,
266+
// ore or ope.
267+
func parseOperatorIndex(s string) (record.Output, error) {
268+
switch o := record.Output(s); o {
269+
case record.Equality, record.Match, record.Ore, record.Ope:
270+
return o, nil
271+
}
272+
return "", fmt.Errorf("unknown index %q", s)
273+
}
274+
248275
func targetString(f vcvalue.Field) (string, error) {
249276
s, ok := f.Value.(string)
250277
if !ok {

‎languages/golang/encrypt/query.go‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ func (cph *Cipher) QueryTerm(ctx context.Context, plan *record.Plan, field, oper
147147

148148
// parseQueryTerm reads se_query_term's object: exactly the keys operator,
149149
// fidelity, index, domain (a string or null) and term (bytes, or a list of
150-
// two for between).
150+
// two for between). The operator, fidelity and index take only the values
151+
// the engine writes, as the catalog reader's do.
151152
func parseQueryTerm(decoded any) (QueryTerm, error) {
152153
obj, ok := decoded.(vcvalue.Object)
153154
if !ok {
@@ -163,18 +164,20 @@ func parseQueryTerm(decoded any) (QueryTerm, error) {
163164
var err error
164165
switch f.Key {
165166
case "operator":
166-
t.Operator, err = targetString(f)
167+
var s string
168+
if s, err = targetString(f); err == nil {
169+
t.Operator, err = parseOperatorName(s)
170+
}
167171
case "fidelity":
168172
var s string
169-
s, err = targetString(f)
170-
t.Fidelity = record.Fidelity(s)
171-
if err == nil && t.Fidelity != record.Exact && t.Fidelity != record.Probabilistic {
172-
err = fmt.Errorf("unknown fidelity %q", s)
173+
if s, err = targetString(f); err == nil {
174+
t.Fidelity, err = parseFidelity(s)
173175
}
174176
case "index":
175177
var s string
176-
s, err = targetString(f)
177-
t.Index = record.Output(s)
178+
if s, err = targetString(f); err == nil {
179+
t.Index, err = parseOperatorIndex(s)
180+
}
178181
case "domain":
179182
t.Domain, err = targetOptionalString(f)
180183
case "term":

‎languages/golang/encrypt/query_test.go‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
"errors"
88
"os"
99
"path/filepath"
10+
"reflect"
1011
"testing"
1112

1213
"github.com/cipherstash/stack/languages/golang/encrypt"
1314
"github.com/cipherstash/stack/languages/golang/internal/record"
15+
"github.com/cipherstash/vitaminc/bindings/go/vcvalue"
1416
)
1517

1618
// The query fixture holds the guest's query exports to the Rust results:
@@ -25,6 +27,7 @@ type queryFixture struct {
2527
KeySource struct {
2628
Seed string `json:"seed"`
2729
} `json:"key_source"`
30+
Plan json.RawMessage `json:"plan"`
2831
Queries []struct {
2932
Field string `json:"field"`
3033
Operator string `json:"operator"`
@@ -68,9 +71,38 @@ func readQueryFixture(t *testing.T) queryFixture {
6871
if err := json.Unmarshal(raw, &f); err != nil {
6972
t.Fatal(err)
7073
}
74+
// The terms are derived under queryPlan: a fixture recorded under any
75+
// other plan fails here, by name, rather than as mismatched bytes.
76+
var recorded any
77+
if err := json.Unmarshal(f.Plan, &recorded); err != nil {
78+
t.Fatal(err)
79+
}
80+
if want := wireJSON(queryPlan.Wire()); !reflect.DeepEqual(recorded, want) {
81+
t.Fatalf("the fixture's plan is not queryPlan; update queryPlan to match it:\nfixture: %v\nqueryPlan: %v", recorded, want)
82+
}
7183
return f
7284
}
7385

86+
// wireJSON is a plan's wire form as encoding/json decodes the same JSON:
87+
// objects as maps, lists as []any.
88+
func wireJSON(v any) any {
89+
switch v := v.(type) {
90+
case vcvalue.Object:
91+
out := make(map[string]any, len(v))
92+
for _, f := range v {
93+
out[f.Key] = wireJSON(f.Value)
94+
}
95+
return out
96+
case []any:
97+
out := make([]any, len(v))
98+
for i, item := range v {
99+
out[i] = wireJSON(item)
100+
}
101+
return out
102+
}
103+
return v
104+
}
105+
74106
// queryPlan is the fixture's declaration: an email with equality, an age
75107
// with equality and ORE, a name with match, all under users.
76108
var queryPlan = &record.Plan{Context: []string{"users"}, Fields: []record.Field{

‎languages/golang/encrypt/targets_test.go‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ func TestParseTargetsReadsEachEntry(t *testing.T) {
7575
"unknown arity": withOperators([]any{operator("eq", "exact", "three", "eq")}),
7676
"unknown op index": withOperators([]any{operator("eq", "exact", "one", "json")}),
7777
"no op name": withOperators([]any{operator("", "exact", "one", "eq")}),
78+
"unknown operator": withOperators([]any{operator("like", "exact", "one", "eq")}),
79+
"operator case": withOperators([]any{operator("Eq", "exact", "one", "eq")}),
7880
"op key missing": withOperators([]any{vcvalue.Object{{Key: "operator", Value: "eq"}}}),
7981
"op unknown key": withOperators([]any{append(operator("eq", "exact", "one", "eq"), vcvalue.Field{Key: "case", Value: "insensitive"})}),
8082
"op key twice": withOperators([]any{append(operator("eq", "exact", "one", "eq"), vcvalue.Field{Key: "index", Value: "eq"})}),
@@ -97,3 +99,35 @@ func withOperators(value any) vcvalue.Object {
9799
{Key: "name", Value: "TextEq"}, {Key: "indexes", Value: []any{}}, {Key: "producible", Value: true}, {Key: "operators", Value: value},
98100
}}}}
99101
}
102+
103+
// se_query_term's object is read with the catalog's own parsers: an
104+
// operator, fidelity or index the catalog would refuse is refused here too,
105+
// and every name the engine writes is read by both.
106+
func TestParseQueryTermTakesOnlyWhatTheEngineWrites(t *testing.T) {
107+
term := func(operator, fidelity, index string) vcvalue.Object {
108+
return vcvalue.Object{
109+
{Key: "operator", Value: operator}, {Key: "fidelity", Value: fidelity}, {Key: "index", Value: index},
110+
{Key: "domain", Value: nil}, {Key: "term", Value: []byte{1, 2}},
111+
}
112+
}
113+
for _, name := range []string{"eq", "lt", "lte", "gt", "gte", "between", "matches"} {
114+
got, err := parseQueryTerm(term(name, "exact", "ore"))
115+
if err != nil || got.Operator != name || got.Fidelity != record.Exact || got.Index != record.Ore {
116+
t.Errorf("%s: %+v, %v", name, got, err)
117+
}
118+
if _, err := parseTargets(withOperators([]any{operator(name, "exact", "one", "ore")})); err != nil {
119+
t.Errorf("catalog %s: %v", name, err)
120+
}
121+
}
122+
for name, bad := range map[string]vcvalue.Object{
123+
"unknown operator": term("like", "exact", "eq"),
124+
"operator case": term("Eq", "exact", "eq"),
125+
"unknown fidelity": term("eq", "approximate", "eq"),
126+
"unknown index": term("eq", "exact", "json"),
127+
"misspelled index": term("eq", "exact", "equality"),
128+
} {
129+
if _, err := parseQueryTerm(bad); err == nil {
130+
t.Errorf("%s: read, want refused", name)
131+
}
132+
}
133+
}

‎packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.stderr‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ error[E0277]: `Contains` is not a leaf operator, so it cannot be applied at a pa
99
= help: the trait `Leaf` is not implemented for `Contains`
1010
= note: `op::AtPath` applies `op::Eq`, `Lt`, `Lte`, `Gt`, `Gte`, `Between` or `Matches` at a path; `op::Contains` and `op::ContainedBy` compare whole documents, and an accessor does not nest
1111
= help: the following other types implement trait `Leaf`:
12-
Between
13-
Gt
14-
Gte
15-
Lt
16-
Lte
12+
stack_encrypt::op::Between
1713
stack_encrypt::op::Eq
14+
stack_encrypt::op::Gt
15+
stack_encrypt::op::Gte
16+
stack_encrypt::op::Lt
17+
stack_encrypt::op::Lte
1818
stack_encrypt::op::Matches<C>
1919
= note: required for `AtPath<Contains>` to implement `Operator`
2020
note: required by a bound in `operator`
@@ -34,12 +34,12 @@ error[E0277]: `AtPath<stack_encrypt::op::Eq>` is not a leaf operator, so it cann
3434
= help: the trait `Leaf` is not implemented for `AtPath<stack_encrypt::op::Eq>`
3535
= note: `op::AtPath` applies `op::Eq`, `Lt`, `Lte`, `Gt`, `Gte`, `Between` or `Matches` at a path; `op::Contains` and `op::ContainedBy` compare whole documents, and an accessor does not nest
3636
= help: the following other types implement trait `Leaf`:
37-
Between
38-
Gt
39-
Gte
40-
Lt
41-
Lte
37+
stack_encrypt::op::Between
4238
stack_encrypt::op::Eq
39+
stack_encrypt::op::Gt
40+
stack_encrypt::op::Gte
41+
stack_encrypt::op::Lt
42+
stack_encrypt::op::Lte
4343
stack_encrypt::op::Matches<C>
4444
= note: required for `AtPath<AtPath<stack_encrypt::op::Eq>>` to implement `Operator`
4545
note: required by a bound in `operator`

‎packages/stack-encrypt/tests/ui/op_between_takes_a_pair.stderr‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ note: required by a bound in `plan::chain::<impl StackCipher<K>>::query`
2222
| F<Q>: Input<Op::Arity>,
2323
| ^^^^^^^^^^^^^^^^ required by this bound in `plan::chain::<impl StackCipher<K>>::query`
2424

25-
error[E0599]: the method `on` exists for struct `QueryBuilder<'_, Between, u32, FakeDataKeySource>`, but its trait bounds were not satisfied
25+
error[E0599]: the method `on` exists for struct `QueryBuilder<'_, stack_encrypt::op::Between, u32, FakeDataKeySource>`, but its trait bounds were not satisfied
2626
--> tests/ui/op_between_takes_a_pair.rs:12:47
2727
|
2828
12 | let _ = cipher.query(op::Between, &40u32).on(&age_plan);
@@ -39,7 +39,7 @@ error[E0271]: type mismatch resolving `<(u32, {integer}) as Input<One>>::Plainte
3939
|
4040
= note: expected type `u32`
4141
found tuple `(u32, {integer})`
42-
= note: required for `ValuePlan<u32, Indexed<stack_encrypt::Ore>>` to implement `Chooses<Lt, (u32, {integer}), Whole>`
42+
= note: required for `ValuePlan<u32, Indexed<stack_encrypt::Ore>>` to implement `Chooses<stack_encrypt::op::Lt, (u32, {integer}), Whole>`
4343

4444
error[E0277]: `(u32, {integer})` is not ordered: the ordering operators are answered over the integer types only
4545
--> tests/ui/op_between_takes_a_pair.rs:13:48
@@ -59,5 +59,5 @@ error[E0277]: `(u32, {integer})` is not ordered: the ordering operators are answ
5959
u16
6060
u32
6161
and $N others
62-
= note: required for `Lt` to implement `DefaultIndex<(u32, {integer})>`
63-
= note: required for `ValuePlan<u32, Indexed<stack_encrypt::Ore>>` to implement `Chooses<Lt, (u32, {integer}), Whole>`
62+
= note: required for `stack_encrypt::op::Lt` to implement `DefaultIndex<(u32, {integer})>`
63+
= note: required for `ValuePlan<u32, Indexed<stack_encrypt::Ore>>` to implement `Chooses<stack_encrypt::op::Lt, (u32, {integer}), Whole>`

‎packages/stack-encrypt/tests/ui/op_json_not_answered.stderr‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ error[E0277]: `Contains` has no default index given `std::string::String`
1616
`stack_encrypt::op::Matches<<DefaultMatch as stack_encrypt::op::MatchCase>::Case>` implements `DefaultIndex<S>`
1717
= note: required for `ValuePlan<std::string::String, Indexed<stack_encrypt::Equality>>` to implement `Chooses<Contains, std::string::String, Whole>`
1818

19-
error[E0277]: `(&str, &str)` is not what a query of arity `Pathed<One>` takes
19+
error[E0277]: `(&str, &str)` is not what a query of arity `Pathed<stack_encrypt::op::One>` takes
2020
--> tests/ui/op_json_not_answered.rs:15:36
2121
|
2222
15 | .query(op::AtPath(op::Eq), &("$.address.city", "Sydney"))
@@ -25,13 +25,13 @@ error[E0277]: `(&str, &str)` is not what a query of arity `Pathed<One>` takes
2525
| required by a bound introduced by this call
2626
|
2727
= note: an operator of arity `One` takes one value, `op::Between` (arity `Pair`) takes a pair, `&(lo, hi)`, and `op::AtPath` (arity `Pathed`) takes no input yet: it arrives with the JSON index
28-
help: the trait `Input<Pathed<One>>` is not implemented for `(&str, &str)`
28+
help: the trait `Input<Pathed<stack_encrypt::op::One>>` is not implemented for `(&str, &str)`
2929
but trait `Input<Pair>` is implemented for it
3030
--> src/op/mod.rs
3131
|
3232
| impl<S: 'static> Input<Pair> for (S, S) {
3333
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34-
= help: for that trait implementation, expected `Pair`, found `Pathed<One>`
34+
= help: for that trait implementation, expected `Pair`, found `Pathed<stack_encrypt::op::One>`
3535
note: required by a bound in `plan::chain::<impl StackCipher<K>>::query`
3636
--> src/plan/chain.rs
3737
|
@@ -51,4 +51,4 @@ error[E0599]: the method `on` exists for struct `QueryBuilder<'_, AtPath<stack_e
5151
| |_________-^^
5252
|
5353
= note: the following trait bounds were not satisfied:
54-
`(&str, &str): Input<Pathed<One>>`
54+
`(&str, &str): Input<Pathed<stack_encrypt::op::One>>`

0 commit comments

Comments
 (0)