Add Remote session foundations and feasibility checks
Add opt-in Tentacle protocol 1.1, explicit remote grants, lifecycle IPC and expiring core leases in both SDKs. Revoke access before update preparation, including failed updates. Add transient secret entries, shared Braille cell export and isolated Xorg recovery experiments with focused tests and documented limitations. Production consent, desktop services and live interoperability remain pending.
This commit is contained in:
1 parent
a47253d697
commit
573d15d7b4
45 files changed
+3634
-80
No files matched your search
@@ -0,0 +1,63 @@
|
||||
package braille
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// displayMasks is shared by the BrlAPI writer and cell export. A zero AND mask
|
||||
// makes a Unicode Braille cell independent of BRLTTY's text translation table.
|
||||
// Dots are attributes to OR into the cell, not a replacement cell array.
|
||||
func displayMasks(text string, dots []byte) ([]byte, []byte, error) {
|
||||
if !utf8.ValidString(text) {
|
||||
return nil, nil, errors.New("invalid braille text")
|
||||
}
|
||||
length := utf8.RuneCountInString(text)
|
||||
if len(dots) != 0 && len(dots) != length {
|
||||
return nil, nil, errors.New("invalid braille indicator length")
|
||||
}
|
||||
andMask, orMask := make([]byte, length), make([]byte, length)
|
||||
for index, cell := range []rune(text) {
|
||||
andMask[index] = 0xff
|
||||
if len(dots) != 0 {
|
||||
orMask[index] = dots[index]
|
||||
}
|
||||
if cell >= 0x2800 && cell <= 0x28ff {
|
||||
andMask[index] = 0
|
||||
orMask[index] |= byte(cell - 0x2800)
|
||||
}
|
||||
}
|
||||
return andMask, orMask, nil
|
||||
}
|
||||
|
||||
// rawCells exports a translated viewport without opening a display. It shares
|
||||
// the physical writer's cell/attribute handling and the existing viewport's
|
||||
// translation, wrapping and routing maps. The caller owns this viewport and
|
||||
// serializes all access, independently of any physical viewport.
|
||||
//
|
||||
// Ordinary text needs BRLTTY translation and is deliberately rejected. Callers
|
||||
// must first use the existing liblouis path with an explicitly chosen table.
|
||||
// Spaces here are blank padding or the synthetic end-of-line cursor cell.
|
||||
// cursorDots is an explicit, static overlay; this does not reproduce BRLTTY's
|
||||
// configured cursor shape or blink timing. No native routing metadata is exported.
|
||||
func (v *viewport) rawCells(cursorDots byte) ([]byte, error) {
|
||||
if _, err := (Geometry{Columns: v.width, Rows: 1}).Cells(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text := []rune(v.frame())
|
||||
for index, cell := range text {
|
||||
if cell == ' ' {
|
||||
text[index] = '\u2800'
|
||||
} else if cell < 0x2800 || cell > 0x28ff {
|
||||
return nil, errors.New("raw braille export requires translated cells")
|
||||
}
|
||||
}
|
||||
_, cells, err := displayMasks(string(text), v.displayDots())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cursor := v.displayCursor(); cursor > 0 {
|
||||
cells[cursor-1] |= cursorDots
|
||||
}
|
||||
return cells, nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package braille
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDisplayMasksPreserveEveryRawCell(t *testing.T) {
|
||||
text := make([]rune, 256)
|
||||
dots := make([]byte, 256)
|
||||
for index := range text {
|
||||
text[index] = rune(0x2800 + index)
|
||||
dots[index] = 0x40
|
||||
}
|
||||
andMask, orMask, err := displayMasks(string(text), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for index := range text {
|
||||
if andMask[index] != 0 || orMask[index] != byte(index) {
|
||||
t.Fatalf("cell %d: and=%x or=%x", index, andMask[index], orMask[index])
|
||||
}
|
||||
}
|
||||
andMask, orMask, err = displayMasks(string(text), dots)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for index := range text {
|
||||
if andMask[index] != 0 || orMask[index] != byte(index)|0x40 {
|
||||
t.Fatalf("cell %d lost raw dots or attributes", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayMasksRetainTextTranslation(t *testing.T) {
|
||||
andMask, orMask, err := displayMasks("a界 ⠁", []byte{0x40, 0, 0, 0x80})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(andMask, []byte{0xff, 0xff, 0xff, 0}) ||
|
||||
!bytes.Equal(orMask, []byte{0x40, 0, 0, 0x81}) {
|
||||
t.Fatalf("and=%x or=%x", andMask, orMask)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
text string
|
||||
dots []byte
|
||||
}{{"\xff", nil}, {"界", []byte{1, 2}}, {"", []byte{1}}} {
|
||||
if _, _, err := displayMasks(tc.text, tc.dots); err == nil {
|
||||
t.Fatal("accepted invalid frame")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawCellsCursorPaddingAndSnapshotOwnership(t *testing.T) {
|
||||
v := viewport{width: 4}
|
||||
if err := v.setFrame(Frame{ID: 8, Text: "⠁⠃", Cursor: 2, Dots: []byte{0x40, 0}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cells, err := v.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(cells, []byte{0x41, 3, 0xc0, 0}) {
|
||||
t.Fatalf("cells=%x error=%v", cells, err)
|
||||
}
|
||||
if route, ok := v.route(RouteBase + 2); !ok || route != (RoutingEvent{ID: 8, Offset: 2}) {
|
||||
t.Fatalf("end-of-line route=%+v ok=%v", route, ok)
|
||||
}
|
||||
if _, ok := v.route(RouteBase + 3); ok {
|
||||
t.Fatal("padding has a routing target")
|
||||
}
|
||||
cells[0] = 0xff
|
||||
withoutCursor, err := v.rawCells(0)
|
||||
if err != nil || !bytes.Equal(withoutCursor, []byte{0x41, 3, 0, 0}) {
|
||||
t.Fatalf("snapshot or cursor mutated viewport: %x, %v", withoutCursor, err)
|
||||
}
|
||||
if err := v.setFrame(Frame{ID: 9, Cursor: -1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleared, err := v.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(cleared, make([]byte, 4)) {
|
||||
t.Fatalf("empty replacement=%x, %v", cleared, err)
|
||||
}
|
||||
if _, ok := v.route(RouteBase); ok {
|
||||
t.Fatal("empty replacement retained routing")
|
||||
}
|
||||
if cells[1] != 3 {
|
||||
t.Fatal("replacement changed an earlier snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawCellsRequireTranslationAndValidWidth(t *testing.T) {
|
||||
v := viewport{width: 4}
|
||||
if err := v.setFrame(Frame{Text: "cat", Cursor: -1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.rawCells(0xc0); err == nil {
|
||||
t.Fatal("ordinary text was exported as guessed dots")
|
||||
}
|
||||
for _, width := range []int{-1, 0, 4097} {
|
||||
invalid := viewport{width: width}
|
||||
if _, err := invalid.rawCells(0); err == nil {
|
||||
t.Fatalf("accepted width %d", width)
|
||||
}
|
||||
}
|
||||
v = viewport{width: 4096}
|
||||
if err := v.setFrame(Frame{Cursor: -1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cells, err := v.rawCells(0)
|
||||
if err != nil || len(cells) != 4096 {
|
||||
t.Fatalf("maximum width: %d, %v", len(cells), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawCellsFailureDoesNotAlterViewport(t *testing.T) {
|
||||
v := viewport{width: 2}
|
||||
if err := v.setFrame(Frame{Text: "abcd", Cursor: -1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.pan(PanRight)
|
||||
before := v
|
||||
if _, err := v.rawCells(0xc0); err == nil {
|
||||
t.Fatal("accepted untranslated text")
|
||||
}
|
||||
if !reflect.DeepEqual(v, before) {
|
||||
t.Fatal("failed export changed local presentation")
|
||||
}
|
||||
}
|
||||
@@ -168,23 +168,12 @@ func (n *native) WriteWithDots(text string, cursor int, dots []byte) error {
|
||||
if !utf8.ValidString(text) || utf8.RuneCountInString(text) != n.cells || cursor < 0 || cursor > n.cells {
|
||||
return errors.New("invalid braille frame")
|
||||
}
|
||||
if len(dots) != 0 && len(dots) != n.cells {
|
||||
return errors.New("invalid braille indicator length")
|
||||
andMask, orMask, err := displayMasks(text, dots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value := C.CString(text)
|
||||
defer C.free(unsafe.Pointer(value))
|
||||
andMask := make([]byte, n.cells)
|
||||
orMask := make([]byte, n.cells)
|
||||
for index, cell := range []rune(text) {
|
||||
andMask[index] = 0xff
|
||||
if len(dots) != 0 {
|
||||
orMask[index] = dots[index]
|
||||
}
|
||||
if cell >= 0x2800 && cell <= 0x28ff {
|
||||
andMask[index] = 0
|
||||
orMask[index] |= byte(cell - 0x2800)
|
||||
}
|
||||
}
|
||||
if C.cth_write(n.session, value, C.int(len(text)), C.int(cursor),
|
||||
(*C.uchar)(unsafe.Pointer(&andMask[0])), (*C.uchar)(unsafe.Pointer(&orMask[0]))) < 0 {
|
||||
return errors.New("BrlAPI display write failed")
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
//go:build brlapi && cgo
|
||||
|
||||
package braille
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// These feasibility checks use real liblouis translation and the helper's
|
||||
// viewport, but never construct or open a BrlAPI backend. The two viewports
|
||||
// model independently owned local and remote sessions, not a session API.
|
||||
func TestVirtualBrailleIndependentWidthsAndRouting(t *testing.T) {
|
||||
frame := Frame{ID: 11, Text: "the cat", Cursor: -1, Contract: true,
|
||||
Table: "en-us-g2.ctb", WordWrap: true, Dots: []byte{0, 0x40, 0, 0, 0, 0, 0}}
|
||||
local, remote := viewport{width: 8}, viewport{width: 4}
|
||||
for _, v := range []*viewport{&local, &remote} {
|
||||
if err := v.setFrame(frame); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
localCells, err := local.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(localCells, []byte{0x6e, 0, 9, 1, 0x1e, 0, 0, 0}) {
|
||||
t.Fatalf("local=%x error=%v", localCells, err)
|
||||
}
|
||||
remoteCells, err := remote.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(remoteCells, []byte{0x6e, 0, 0, 0}) {
|
||||
t.Fatalf("wrapped remote=%x error=%v", remoteCells, err)
|
||||
}
|
||||
if route, ok := remote.route(RouteBase + 1); !ok || route != (RoutingEvent{ID: 11, Offset: 3}) {
|
||||
t.Fatalf("contracted route=%+v ok=%v", route, ok)
|
||||
}
|
||||
if _, ok := remote.route(RouteBase + 2); ok {
|
||||
t.Fatal("wrapped padding routed into hidden text")
|
||||
}
|
||||
if !remote.pan(PanRight) {
|
||||
t.Fatal("remote did not pan")
|
||||
}
|
||||
panned, err := remote.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(panned, []byte{9, 1, 0x1e, 0}) {
|
||||
t.Fatalf("panned=%x error=%v", panned, err)
|
||||
}
|
||||
if route, ok := remote.route(RouteBase); !ok || route != (RoutingEvent{ID: 11, Offset: 4}) {
|
||||
t.Fatalf("panned route=%+v ok=%v", route, ok)
|
||||
}
|
||||
if !remote.pan(PanLeft) {
|
||||
t.Fatal("remote did not pan back")
|
||||
}
|
||||
restored, err := remote.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(restored, remoteCells) {
|
||||
t.Fatalf("restored=%x error=%v", restored, err)
|
||||
}
|
||||
// A new width gets fresh state. No physical geometry is changed and no
|
||||
// old pan offset or routing map is carried across the replacement.
|
||||
remote = viewport{width: 2}
|
||||
frame.ID = 12
|
||||
if err := remote.setFrame(frame); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if route, ok := remote.route(RouteBase); !ok || route.ID != 12 {
|
||||
t.Fatal("replacement retained an old frame identity")
|
||||
}
|
||||
unchanged, err := local.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(unchanged, localCells) || local.offset != 0 || local.width != 8 || local.id != 11 {
|
||||
t.Fatal("remote pan/resize altered local viewport")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualBrailleCursorUsesTranslatedPosition(t *testing.T) {
|
||||
v := viewport{width: 4}
|
||||
if err := v.setFrame(Frame{Text: "the cat", Cursor: 4, Contract: true, Table: "en-us-g2.ctb"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cells, err := v.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(cells, []byte{0x2e, 0, 0xc9, 1}) {
|
||||
t.Fatalf("translated cursor=%x error=%v", cells, err)
|
||||
}
|
||||
if !v.pan(PanRight) {
|
||||
t.Fatal("did not pan past cursor")
|
||||
}
|
||||
cells, err = v.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(cells, []byte{0x1e, 0, 0, 0}) {
|
||||
t.Fatalf("hidden cursor=%x error=%v", cells, err)
|
||||
}
|
||||
if err := v.setFrame(Frame{ID: 14, Text: "the cat", Cursor: 7, Contract: true, Table: "en-us-g2.ctb"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cells, err = v.rawCells(0xc0)
|
||||
if err != nil || !bytes.Equal(cells, []byte{0x1e, 0xc0, 0, 0}) {
|
||||
t.Fatalf("end cursor=%x error=%v", cells, err)
|
||||
}
|
||||
if route, ok := v.route(RouteBase + 1); !ok || route != (RoutingEvent{ID: 14, Offset: 7}) {
|
||||
t.Fatalf("end route=%+v ok=%v", route, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualBrailleExplicitComputerTableAndFailure(t *testing.T) {
|
||||
v := viewport{width: 4}
|
||||
// Contract selects the helper's liblouis path; the table determines whether
|
||||
// it contracts. A future virtual-helper API must resolve this distinction.
|
||||
if err := v.setFrame(Frame{ID: 15, Text: "cat", Cursor: -1, Contract: true, Table: "en-us-comp8.ctb"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cells, err := v.rawCells(0)
|
||||
if err != nil || !bytes.Equal(cells, []byte{9, 1, 0x1e, 0}) {
|
||||
t.Fatalf("computer table=%x error=%v", cells, err)
|
||||
}
|
||||
before := v
|
||||
if err := v.setFrame(Frame{Text: "cat", Cursor: -1, Contract: true, Table: "missing-remote-test-table.ctb"}); err == nil {
|
||||
t.Fatal("missing table accepted")
|
||||
}
|
||||
if !reflect.DeepEqual(v, before) {
|
||||
t.Fatal("failed translation replaced a valid viewport")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user