detached b970441

refactor(headless): share the screenshot security gate

commit
b9704414c3d166506625aba3985d014e6588f008
author
brice · · 2026-10-22 04:53 UTC
parent
9ccafd9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +14 -7
M internal/adapters/headless/output.go +14 -7
@@ -642,9 +642,10 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity
642642 return err
643643 }
644644
645-// screenshotPNG encodes screenshot frames: encoding is by far the largest cost
646-// of a headless frame, so it trades file size for speed. Its buffers are
647-// reused between frames.
645+// screenshotPNG encodes the PNGs written by the headless output (frames, the
646+// latest.png fallback, raw HDR latest-pq.png): encoding is by far the largest
647+// cost of a headless frame, so it trades file size for speed. Its buffers are
648+// reused between files.
648649 var screenshotPNG = png.Encoder{CompressionLevel: png.BestSpeed, BufferPool: new(pngBuffers)}
649650
650651 // pngBuffers is a png.EncoderBufferPool.
@@ -657,9 +658,15 @@ func (p *pngBuffers) Get() *png.EncoderBuffer {
657658
658659 func (p *pngBuffers) Put(b *png.EncoderBuffer) { p.pool.Put(b) }
659660
661+// gated reports whether a screenshot must not be published: the session is
662+// protected, or its security state is no longer the one the frame was drawn in.
663+func gated(security ports.SessionSecurity, state ports.SecurityState) bool {
664+ return security != nil && (state.Protected || security.Snapshot() != state)
665+}
666+
660667 // writePNGSecureResult distinguishes a security skip from a published PNG.
661668 func writePNGSecureResult(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
662- if security != nil && (state.Protected || security.Snapshot() != state) {
669+ if gated(security, state) {
663670 return false, nil
664671 }
665672 f, err := os.CreateTemp(filepath.Dir(path), ".frame-*.png")
@@ -674,7 +681,7 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
674681 if err := f.Close(); err != nil {
675682 return false, err
676683 }
677- if security != nil && security.Snapshot() != state {
684+ if gated(security, state) {
678685 return false, nil
679686 }
680687 if err := os.Rename(f.Name(), path); err != nil {
@@ -690,7 +697,7 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
690697 // It gates on security like writePNGSecureResult, and falls back to encoding
691698 // img where the file system cannot link.
692699 func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
693- if security != nil && (state.Protected || security.Snapshot() != state) {
700+ if gated(security, state) {
694701 return false, nil
695702 }
696703 tmp := dst + ".tmp"
@@ -699,7 +706,7 @@ func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecur
699706 return writePNGSecureResult(dst, img, security, state)
700707 }
701708 defer os.Remove(tmp)
702- if security != nil && security.Snapshot() != state {
709+ if gated(security, state) {
703710 return false, nil
704711 }
705712 if err := os.Rename(tmp, dst); err != nil {
@@ -642,9 +642,10 @@ func writePNGSecure(path string, img image.Image, security ports.SessionSecurity
642 return err642 return err
643 }643 }
644 644
645-// screenshotPNG encodes screenshot frames: encoding is by far the largest cost645+// screenshotPNG encodes the PNGs written by the headless output (frames, the
646-// of a headless frame, so it trades file size for speed. Its buffers are646+// latest.png fallback, raw HDR latest-pq.png): encoding is by far the largest
647-// reused between frames.647+// cost of a headless frame, so it trades file size for speed. Its buffers are
648+// reused between files.
648 var screenshotPNG = png.Encoder{CompressionLevel: png.BestSpeed, BufferPool: new(pngBuffers)}649 var screenshotPNG = png.Encoder{CompressionLevel: png.BestSpeed, BufferPool: new(pngBuffers)}
649 650
650 // pngBuffers is a png.EncoderBufferPool.651 // pngBuffers is a png.EncoderBufferPool.
@@ -657,9 +658,15 @@ func (p *pngBuffers) Get() *png.EncoderBuffer {
657 658
658 func (p *pngBuffers) Put(b *png.EncoderBuffer) { p.pool.Put(b) }659 func (p *pngBuffers) Put(b *png.EncoderBuffer) { p.pool.Put(b) }
659 660
661+// gated reports whether a screenshot must not be published: the session is
662+// protected, or its security state is no longer the one the frame was drawn in.
663+func gated(security ports.SessionSecurity, state ports.SecurityState) bool {
664+ return security != nil && (state.Protected || security.Snapshot() != state)
665+}
666+
660 // writePNGSecureResult distinguishes a security skip from a published PNG.667 // writePNGSecureResult distinguishes a security skip from a published PNG.
661 func writePNGSecureResult(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {668 func writePNGSecureResult(path string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
662- if security != nil && (state.Protected || security.Snapshot() != state) {669+ if gated(security, state) {
663 return false, nil670 return false, nil
664 }671 }
665 f, err := os.CreateTemp(filepath.Dir(path), ".frame-*.png")672 f, err := os.CreateTemp(filepath.Dir(path), ".frame-*.png")
@@ -674,7 +681,7 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
674 if err := f.Close(); err != nil {681 if err := f.Close(); err != nil {
675 return false, err682 return false, err
676 }683 }
677- if security != nil && security.Snapshot() != state {684+ if gated(security, state) {
678 return false, nil685 return false, nil
679 }686 }
680 if err := os.Rename(f.Name(), path); err != nil {687 if err := os.Rename(f.Name(), path); err != nil {
@@ -690,7 +697,7 @@ func writePNGSecureResult(path string, img image.Image, security ports.SessionSe
690 // It gates on security like writePNGSecureResult, and falls back to encoding697 // It gates on security like writePNGSecureResult, and falls back to encoding
691 // img where the file system cannot link.698 // img where the file system cannot link.
692 func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {699 func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecurity, state ports.SecurityState) (bool, error) {
693- if security != nil && (state.Protected || security.Snapshot() != state) {700+ if gated(security, state) {
694 return false, nil701 return false, nil
695 }702 }
696 tmp := dst + ".tmp"703 tmp := dst + ".tmp"
@@ -699,7 +706,7 @@ func linkPNGSecure(src, dst string, img image.Image, security ports.SessionSecur
699 return writePNGSecureResult(dst, img, security, state)706 return writePNGSecureResult(dst, img, security, state)
700 }707 }
701 defer os.Remove(tmp)708 defer os.Remove(tmp)
702- if security != nil && security.Snapshot() != state {709+ if gated(security, state) {
703 return false, nil710 return false, nil
704 }711 }
705 if err := os.Rename(tmp, dst); err != nil {712 if err := os.Rename(tmp, dst); err != nil {

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 24

Tags 10

↑↓ moveenter openesc close