diff options
Diffstat (limited to 'internal/cond')
| -rw-r--r-- | internal/cond/eval.go | 139 | ||||
| -rw-r--r-- | internal/cond/eval_test.go | 33 |
2 files changed, 131 insertions, 41 deletions
diff --git a/internal/cond/eval.go b/internal/cond/eval.go index f241504..c9f7f75 100644 --- a/internal/cond/eval.go +++ b/internal/cond/eval.go @@ -34,9 +34,11 @@ type Result struct { Reasons []string // what made it true, e.g. `type pdf`, `content "acme ltd"`, `name "\bacme\b"` Warnings []string // e.g. `content unreadable: needs pdftotext, not installed` - // Unreadable is true when evaluation reached a content test that could - // not read the file. The test counts as false; an exclude uses this to - // hold anyway (review M11). + // Unreadable is true when the condition's value is unknown: it depends + // on a content test that could not read the file. Match is then false; + // an exclude holds anyway (review M11). A condition decided whatever the + // text holds - (and (content "x") (type txt)) on a pdf - is not + // unknown (plan 11). Unreadable bool } @@ -44,18 +46,29 @@ type Result struct { type Trace struct { Label string Value bool + Unknown bool // the value depends on a content test that could not read the file Err string Children []*Trace } +// tri is a three-valued truth value: a content test that cannot read its +// file is unknown, and and/or/not follow Kleene's logic, so an unknown only +// spreads where the text could change the answer. +type tri int8 + +const ( + no tri = iota + yes + unknown +) + // evalCtx accumulates state across one Eval call: the captures of the // first true, non-negated name test, and warnings de-duplicated in // first-seen order. type evalCtx struct { - captures []string - warned map[string]bool - warnings []string - unreadable bool + captures []string + warned map[string]bool + warnings []string } // warn records msg unless it has already been recorded. @@ -80,38 +93,54 @@ func (c *Cond) Eval(f Facts) Result { return Result{Match: true, Reasons: []string{"no condition"}} } ctx := &evalCtx{} - match, reasons := c.eval(c.root, f, ctx, false) - return Result{Match: match, Captures: ctx.captures, Reasons: reasons, Warnings: ctx.warnings, Unreadable: ctx.unreadable} + v, reasons := c.eval(c.root, f, ctx, false) + return Result{Match: v == yes, Captures: ctx.captures, Reasons: reasons, Warnings: ctx.warnings, Unreadable: v == unknown} } // eval evaluates one node against f, short-circuiting and/or in child // (cost-sorted) order. negated tracks whether n is reached under an odd // number of enclosing nots, so a matching name test found there does not // supply Result.Captures. -func (c *Cond) eval(n *node, f Facts, ctx *evalCtx, negated bool) (bool, []string) { +func (c *Cond) eval(n *node, f Facts, ctx *evalCtx, negated bool) (tri, []string) { switch n.kind { case kAnd: + // A false child decides; an unknown one does not, so the rest still + // run (one of them may be false). var reasons []string + v := yes for _, ch := range n.children { - ok, r := c.eval(ch, f, ctx, negated) - if !ok { - return false, nil + cv, r := c.eval(ch, f, ctx, negated) + switch cv { + case no: + return no, nil + case unknown: + v = unknown } reasons = append(reasons, r...) } - return true, reasons + if v == unknown { + return unknown, nil + } + return yes, reasons case kOr: + v := no for _, ch := range n.children { - if ok, r := c.eval(ch, f, ctx, negated); ok { - return true, r + cv, r := c.eval(ch, f, ctx, negated) + switch cv { + case yes: + return yes, r + case unknown: + v = unknown } } - return false, nil + return v, nil case kNot: child := n.children[0] - ok, _ := c.eval(child, f, ctx, !negated) - if ok { - return false, nil + switch cv, _ := c.eval(child, f, ctx, !negated); cv { + case yes: + return no, nil + case unknown: + return unknown, nil } // E4: a negated leaf reads fine as "not " plus the leaf's own // label ("not matched", "not type pdf"), but a negated and/or's @@ -123,22 +152,20 @@ func (c *Cond) eval(n *node, f Facts, ctx *evalCtx, negated bool) (bool, []strin if child.kind == kAnd || child.kind == kOr { label = "(" + child.label + " ...)" } - return true, []string{"not " + label} + return yes, []string{"not " + label} default: ok, reason, warn, caps := c.evalLeaf(n, f) - if warn != "" { - ctx.warn(warn) - if n.kind == kContent { - ctx.unreadable = true - } + ctx.warn(warn) + if warn != "" && n.kind == kContent { + return unknown, nil } if !ok { - return false, nil + return no, nil } if caps != nil && !negated && ctx.captures == nil { ctx.captures = caps } - return true, []string{reason} + return yes, []string{reason} } } @@ -288,28 +315,61 @@ func (c *Cond) explain(n *node, f Facts) *Trace { switch n.kind { case kAnd, kOr: t := &Trace{Label: n.label} - val := n.kind == kAnd // identity: and starts true, or starts false + // Kleene: and is false on any false child, or on any true one. + decide, other := no, yes + if n.kind == kOr { + decide, other = yes, no + } + v := other for _, ch := range n.children { ct := c.explain(ch, f) t.Children = append(t.Children, ct) - if n.kind == kAnd { - val = val && ct.Value - } else { - val = val || ct.Value + switch cv := ct.tri(); { + case cv == decide: + v = decide + case cv == unknown && v != decide: + v = unknown } } - t.Value = val + t.set(v) return t case kNot: ct := c.explain(n.children[0], f) - return &Trace{Label: n.label, Value: !ct.Value, Children: []*Trace{ct}} + t := &Trace{Label: n.label, Children: []*Trace{ct}} + switch ct.tri() { + case yes: + t.set(no) + case no: + t.set(yes) + default: + t.set(unknown) + } + return t default: ok, _, warn, _ := c.evalLeaf(n, f) - return &Trace{Label: n.label, Value: ok, Err: warn} + t := &Trace{Label: n.label, Value: ok, Err: warn} + if warn != "" && n.kind == kContent { + t.set(unknown) + } + return t } } -// Format writes one line per node: "yes"/"no " padded to three, two +func (t *Trace) tri() tri { + switch { + case t.Unknown: + return unknown + case t.Value: + return yes + } + return no +} + +func (t *Trace) set(v tri) { + t.Value, t.Unknown = v == yes, v == unknown +} + +// Format writes one line per node: "yes", "no" or "?" (unknown) padded to three, two // spaces, two spaces of indent per depth, the label, and " (Err)" when // Err is set. func (t *Trace) Format(w io.Writer) { @@ -318,7 +378,10 @@ func (t *Trace) Format(w io.Writer) { func (t *Trace) format(w io.Writer, depth int) { word := "no" - if t.Value { + switch { + case t.Unknown: + word = "?" + case t.Value: word = "yes" } fmt.Fprintf(w, "%-3s %s%s", word, strings.Repeat(" ", depth), t.Label) diff --git a/internal/cond/eval_test.go b/internal/cond/eval_test.go index 2e8bcfd..7d796a1 100644 --- a/internal/cond/eval_test.go +++ b/internal/cond/eval_test.go @@ -167,11 +167,11 @@ func TestExplainFormat(t *testing.T) { c, _ := Compile("d.conf", nodes(t, `(type pdf) (or (content "acme ltd") (name "\bacme\b"))`), Options{IgnoreCase: true}) var b strings.Builder c.Explain(f).Format(&b) - want := "no and\n" + + want := "? and\n" + "yes type pdf\n" + - "no or\n" + + "? or\n" + "no name \"\\bacme\\b\"\n" + - "no content \"acme ltd\" (content unreadable: needs pdftotext, not installed)\n" + "? content \"acme ltd\" (content unreadable: needs pdftotext, not installed)\n" if b.String() != want { t.Fatalf("got\n%s\nwant\n%s", b.String(), want) } @@ -257,3 +257,30 @@ func TestCapturesKeepDiacritics(t *testing.T) { t.Errorf("captures with a group that did not take part = %q, want %q", r.Captures, want) } } + +// TestUnreadableContentIsUnknown: a content test that cannot read the file +// is unknown, not false, and and/or/not combine unknowns the way Kleene's +// three-valued logic does: a condition certainly false (or true) whatever +// the text holds is decided, and only one that depends on the text is +// unknown (plan 11, re-review cache F2). A rule matches only a true +// condition; an exclude holds on true or unknown. +func TestUnreadableContentIsUnknown(t *testing.T) { + f := &fake{name: "a.pdf", rawErr: errors.New("larger than max-read")} + cases := []struct { + src string + match, unreadable bool + }{ + {`(and (content "x") (type txt))`, false, false}, + {`(or (content "x") (type pdf))`, true, false}, + {`(not (content "x"))`, false, true}, + {`(and (type pdf) (content "x"))`, false, true}, + {`(or (type txt) (content "x"))`, false, true}, + {`(content "secret") (and (content "other") (type txt))`, false, false}, + {`(not (and (content "x") (type txt)))`, true, false}, + } + for _, c := range cases { + if r := eval(t, c.src, Options{}, f); r.Match != c.match || r.Unreadable != c.unreadable { + t.Errorf("%s: Match %v Unreadable %v; want %v, %v", c.src, r.Match, r.Unreadable, c.match, c.unreadable) + } + } +} |
