policy.go RawHistoryFolder

1// Package captureallow reads the screen-capture allowlist: the executables
2// that may capture the screen. The file is /etc/neferwl/capture-allow, one
3// absolute executable path per line; it is root-owned and fixed, never read
4// from the user config that a program of the same user could edit.
5package captureallow
6
7import (
8	"bufio"
9	"errors"
10	"fmt"
11	"io"
12	"maps"
13	"path/filepath"
14	"strings"
15)
16
17// DefaultPath is where the allowlist lives.
18const DefaultPath = "/etc/neferwl/capture-allow"
19
20// maxSize bounds what is read: a bigger file is invalid.
21const maxSize = 1 << 20
22
23// Mode is how a policy decides.
24type Mode int
25
26const (
27	// ModeList allows the listed executables only.
28	ModeList Mode = iota
29	// ModeDisabled allows every client: a `*` line.
30	ModeDisabled
31	// ModeClosed allows no client: the file is unreadable or invalid.
32	ModeClosed
33)
34
35// Policy is an immutable allowlist. The zero value allows nothing.
36type Policy struct {
37	mode Mode
38	exes map[string]struct{}
39}
40
41// Builtin returns the built-in list policy, used when the file is missing. A
42// file replaces the list entirely. The list is built on each call, so no
43// package state can be changed.
44func Builtin() *Policy {
45	builtin := [...]string{
46		"/usr/bin/grim",
47		"/usr/bin/nefercap",
48		"/usr/lib/xdg-desktop-portal-wlr",
49		"/usr/libexec/xdg-desktop-portal-wlr",
50	}
51	p := &Policy{mode: ModeList, exes: make(map[string]struct{}, len(builtin))}
52	for _, e := range builtin {
53		p.exes[e] = struct{}{}
54	}
55	return p
56}
57
58// Disabled returns the policy that lets every client capture.
59func Disabled() *Policy { return &Policy{mode: ModeDisabled} }
60
61// Closed returns the policy that lets no client capture.
62func Closed() *Policy { return &Policy{mode: ModeClosed} }
63
64// Mode reports how the policy decides.
65func (p *Policy) Mode() Mode { return p.mode }
66
67// Len is the number of listed executables.
68func (p *Policy) Len() int { return len(p.exes) }
69
70func (p *Policy) equal(o *Policy) bool {
71	return p.mode == o.mode && maps.Equal(p.exes, o.exes)
72}
73
74// Allows reports whether the executable at path may capture. The comparison
75// is exact, on the cleaned absolute path.
76func (p *Policy) Allows(exe string) bool {
77	switch p.mode {
78	case ModeDisabled:
79		return true
80	case ModeClosed:
81		return false
82	}
83	if !filepath.IsAbs(exe) {
84		return false
85	}
86	_, ok := p.exes[filepath.Clean(exe)]
87	return ok
88}
89
90// ParseError names the offending line of an invalid file.
91type ParseError struct {
92	Line int
93	Text string
94	Msg  string
95}
96
97func (e *ParseError) Error() string {
98	return fmt.Sprintf("line %d: %s: %q", e.Line, e.Msg, e.Text)
99}
100
101// Parse reads the file format:
102//
103//   - blank lines and lines starting with `#` are ignored;
104//   - a line `*` alone disables the check;
105//   - any other line is an absolute executable path, taken whole after
106//     trimming surrounding white space (a `#` inside a line is part of the
107//     path).
108//
109// Any other content is an error naming its line; the policy is then nil.
110func Parse(r io.Reader) (*Policy, error) {
111	p := &Policy{mode: ModeList, exes: map[string]struct{}{}}
112	all := false
113	sc := bufio.NewScanner(io.LimitReader(r, maxSize+1))
114	sc.Buffer(make([]byte, 0, 4096), maxSize+1)
115	n, read := 0, 0
116	for sc.Scan() {
117		n++
118		raw := sc.Text()
119		read += len(raw) + 1
120		if read > maxSize {
121			return nil, &ParseError{Line: n, Text: clip(raw), Msg: "file too large"}
122		}
123		line := strings.TrimSpace(raw)
124		switch {
125		case line == "" || strings.HasPrefix(line, "#"):
126		case line == "*":
127			all = true
128		case !filepath.IsAbs(line) || strings.ContainsRune(line, 0) || strings.HasSuffix(line, "/"):
129			return nil, &ParseError{Line: n, Text: clip(line), Msg: "want an absolute executable path"}
130		default:
131			p.exes[filepath.Clean(line)] = struct{}{}
132		}
133	}
134	if err := sc.Err(); err != nil {
135		if errors.Is(err, bufio.ErrTooLong) {
136			return nil, &ParseError{Line: n + 1, Msg: "file too large"}
137		}
138		return nil, err
139	}
140	if all {
141		return Disabled(), nil
142	}
143	return p, nil
144}
145
146func clip(s string) string {
147	if len(s) > 120 {
148		return s[:120] + "..."
149	}
150	return s
151}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close