Add quarantined Xorg destination-key ownership and cleanup
This commit is contained in:
1 parent
792cac5d0b
commit
cd9ae05bbe
10 files changed
+988
-29
No files matched your search
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package input
|
||||
|
||||
import "errors"
|
||||
|
||||
const RemoteHeldKeyLimit = 32
|
||||
|
||||
var (
|
||||
ErrRemoteKeyState = errors.New("remote input owner is retired")
|
||||
ErrRemoteKeyUnavailable = errors.New("remote input key is unavailable")
|
||||
ErrRemoteKeyCollision = errors.New("remote input key ownership is ambiguous")
|
||||
ErrRemoteKeyBackend = errors.New("remote input operation failed")
|
||||
ErrRemoteKeyLimit = errors.New("remote input held-key limit exceeded")
|
||||
)
|
||||
|
||||
// RemoteKeyBackend belongs to one serial native owner. Set can fail after an
|
||||
// effect reached the server; the ledger therefore reserves before down and
|
||||
// retains ownership until up succeeds. This interface supplies no consent,
|
||||
// watchdog, physical-source tracking or independent crash recovery. Resolve
|
||||
// returns an Xorg destination code in 8..255, never a VK or scan code.
|
||||
type RemoteKeyBackend interface {
|
||||
Resolve(name string) (uint16, error)
|
||||
Held(code uint16) (bool, error)
|
||||
Set(code uint16, pressed bool) error
|
||||
}
|
||||
|
||||
// RemoteKeyLedger retains destination codes, not source keycodes or relay VKs.
|
||||
// One instance belongs to one native helper lifetime; its caller must serialize
|
||||
// all methods, validate the original lease at delivery and retire on revoke.
|
||||
type RemoteKeyLedger struct {
|
||||
backend RemoteKeyBackend
|
||||
held map[string]uint16
|
||||
order []string
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func NewRemoteKeyLedger(backend RemoteKeyBackend) (*RemoteKeyLedger, error) {
|
||||
if backend == nil {
|
||||
return nil, ErrRemoteKeyUnavailable
|
||||
}
|
||||
return &RemoteKeyLedger{backend: backend, held: make(map[string]uint16)}, nil
|
||||
}
|
||||
|
||||
func (ledger *RemoteKeyLedger) fail(err error) error {
|
||||
ledger.stopped = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (ledger *RemoteKeyLedger) Apply(name string, pressed bool) error {
|
||||
if ledger == nil || ledger.stopped {
|
||||
return ErrRemoteKeyState
|
||||
}
|
||||
if !ValidRemoteKeyName(name) {
|
||||
return ledger.fail(ErrRemoteKeyUnavailable)
|
||||
}
|
||||
code, owned := ledger.held[name]
|
||||
if owned {
|
||||
if pressed {
|
||||
// Idempotent duplicate downs. Native repeat synthesis/lock policy
|
||||
// remain unavailable; do not generate extra transitions here.
|
||||
return nil
|
||||
}
|
||||
if err := ledger.backend.Set(code, false); err != nil {
|
||||
return ledger.fail(ErrRemoteKeyBackend)
|
||||
}
|
||||
delete(ledger.held, name)
|
||||
for index, held := range ledger.order {
|
||||
if held == name {
|
||||
copy(ledger.order[index:], ledger.order[index+1:])
|
||||
ledger.order[len(ledger.order)-1] = ""
|
||||
ledger.order = ledger.order[:len(ledger.order)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !pressed {
|
||||
return nil // An unowned up must not release a physical/local key.
|
||||
}
|
||||
if name == "Caps_Lock" || name == "Num_Lock" || name == "Scroll_Lock" {
|
||||
// Lock synchronization and reader-modifier handling need their own
|
||||
// native policy. The quarantined backend must not toggle local locks.
|
||||
return ledger.fail(ErrRemoteKeyUnavailable)
|
||||
}
|
||||
if len(ledger.held) == RemoteHeldKeyLimit {
|
||||
return ledger.fail(ErrRemoteKeyLimit)
|
||||
}
|
||||
code, err := ledger.backend.Resolve(name)
|
||||
if err != nil || code < 8 || code > 255 {
|
||||
return ledger.fail(ErrRemoteKeyUnavailable)
|
||||
}
|
||||
for _, other := range ledger.held {
|
||||
if other == code {
|
||||
return ledger.fail(ErrRemoteKeyCollision)
|
||||
}
|
||||
}
|
||||
held, err := ledger.backend.Held(code)
|
||||
if err != nil {
|
||||
return ledger.fail(ErrRemoteKeyBackend)
|
||||
}
|
||||
if held {
|
||||
return ledger.fail(ErrRemoteKeyCollision)
|
||||
}
|
||||
ledger.held[name] = code
|
||||
ledger.order = append(ledger.order, name)
|
||||
if err := ledger.backend.Set(code, true); err != nil {
|
||||
return ledger.fail(ErrRemoteKeyBackend)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retire stops admission permanently and releases only reserved target codes,
|
||||
// in reverse down order. Failed ups retain their identity for explicit cleanup
|
||||
// retry; a successful repeat call has no effects. Cleanup must complete before
|
||||
// destroying the native backend. Never resume a retired ledger.
|
||||
func (ledger *RemoteKeyLedger) Retire() error {
|
||||
if ledger == nil {
|
||||
return ErrRemoteKeyState
|
||||
}
|
||||
ledger.stopped = true
|
||||
failed := false
|
||||
for index := len(ledger.order) - 1; index >= 0; index-- {
|
||||
name := ledger.order[index]
|
||||
if err := ledger.backend.Set(ledger.held[name], false); err != nil {
|
||||
failed = true
|
||||
} else {
|
||||
delete(ledger.held, name)
|
||||
}
|
||||
}
|
||||
remaining := ledger.order[:0]
|
||||
for _, name := range ledger.order {
|
||||
if _, owned := ledger.held[name]; owned {
|
||||
remaining = append(remaining, name)
|
||||
}
|
||||
}
|
||||
clear(ledger.order[len(remaining):])
|
||||
ledger.order = remaining
|
||||
if failed {
|
||||
return ErrRemoteKeyBackend
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user