detached 1667cd9

fix(protocol): enforce encoded catalogue limit

commit
1667cd9080a0a864368676504d8415d406ca9932
author
brice · · 2026-08-28 16:22 UTC
parent
7295b7b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +41 -0
M internal/protocol/catalogue/catalogue.go +9 -0
@@ -2,6 +2,7 @@
22 package catalogue
33
44 import (
5+ "encoding/json"
56 "errors"
67 "fmt"
78 "math"
@@ -213,6 +214,14 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
213214 if bytes > RemoteCatalogMaxCatalogBytes {
214215 return ErrRemoteCatalogTooLarge
215216 }
217+ encoded, err := json.Marshal(c)
218+ if err != nil {
219+ return fmt.Errorf("%w: encoding catalogue: %v", ErrInvalidRemoteCatalog, err)
220+ }
221+ // marshalListing appends one newline after the JSON payload.
222+ if len(encoded)+1 > RemoteCatalogMaxCatalogBytes {
223+ return ErrRemoteCatalogTooLarge
224+ }
216225 return nil
217226 }
218227
@@ -2,6 +2,7 @@
2 package catalogue2 package catalogue
3 3
4 import (4 import (
5+ "encoding/json"
5 "errors"6 "errors"
6 "fmt"7 "fmt"
7 "math"8 "math"
@@ -213,6 +214,14 @@ func ValidateRemoteCatalog(c RemoteCatalog) error {
213 if bytes > RemoteCatalogMaxCatalogBytes {214 if bytes > RemoteCatalogMaxCatalogBytes {
214 return ErrRemoteCatalogTooLarge215 return ErrRemoteCatalogTooLarge
215 }216 }
217+ encoded, err := json.Marshal(c)
218+ if err != nil {
219+ return fmt.Errorf("%w: encoding catalogue: %v", ErrInvalidRemoteCatalog, err)
220+ }
221+ // marshalListing appends one newline after the JSON payload.
222+ if len(encoded)+1 > RemoteCatalogMaxCatalogBytes {
223+ return ErrRemoteCatalogTooLarge
224+ }
216 return nil225 return nil
217 }226 }
218 227
M internal/protocol/catalogue/catalogue_test.go +32 -0
@@ -1,6 +1,9 @@
11 package catalogue
22
33 import (
4+ "encoding/json"
5+ "fmt"
6+ "strings"
47 "testing"
58 "time"
69
@@ -69,6 +72,35 @@ func TestValidateRemoteCatalogCacheEntriesRejectsTerminalUnsafeHost(t *testing.T
6972 require.ErrorIs(t, err, ErrInvalidRemoteCatalog)
7073 }
7174
75+func TestValidateRemoteCatalogRejectsOversizedEncodedJSON(t *testing.T) {
76+ sessions := make([]RemoteCatalogSession, 4)
77+ for sessionIndex := range sessions {
78+ tabs := make([]RemoteCatalogTab, 100)
79+ for tabIndex := range tabs {
80+ tabs[tabIndex] = RemoteCatalogTab{
81+ ID: fmt.Sprintf("tab-%d-%d", sessionIndex, tabIndex),
82+ Index: uint16(tabIndex),
83+ Detail: strings.Repeat("<", RemoteCatalogMaxDetailBytes),
84+ }
85+ }
86+ sessions[sessionIndex] = RemoteCatalogSession{
87+ LifecycleID: [16]byte{byte(sessionIndex + 1)},
88+ Name: fmt.Sprintf("work-%d", sessionIndex),
89+ State: RemoteCatalogSessionUp,
90+ Tabs: tabs,
91+ }
92+ }
93+ catalog := RemoteCatalog{
94+ ProtocolVersion: protocol.Version,
95+ SchemaVersion: RemoteCatalogSchemaVersion,
96+ Sessions: sessions,
97+ }
98+ encoded, err := json.Marshal(catalog)
99+ require.NoError(t, err)
100+ require.Greater(t, len(encoded)+1, RemoteCatalogMaxCatalogBytes)
101+ require.ErrorIs(t, ValidateRemoteCatalog(catalog), ErrRemoteCatalogTooLarge)
102+}
103+
72104 func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) {
73105 err := ValidateRemoteCatalog(RemoteCatalog{
74106 ProtocolVersion: protocol.Version,
@@ -1,6 +1,9 @@
1 package catalogue1 package catalogue
2 2
3 import (3 import (
4+ "encoding/json"
5+ "fmt"
6+ "strings"
4 "testing"7 "testing"
5 "time"8 "time"
6 9
@@ -69,6 +72,35 @@ func TestValidateRemoteCatalogCacheEntriesRejectsTerminalUnsafeHost(t *testing.T
69 require.ErrorIs(t, err, ErrInvalidRemoteCatalog)72 require.ErrorIs(t, err, ErrInvalidRemoteCatalog)
70 }73 }
71 74
75+func TestValidateRemoteCatalogRejectsOversizedEncodedJSON(t *testing.T) {
76+ sessions := make([]RemoteCatalogSession, 4)
77+ for sessionIndex := range sessions {
78+ tabs := make([]RemoteCatalogTab, 100)
79+ for tabIndex := range tabs {
80+ tabs[tabIndex] = RemoteCatalogTab{
81+ ID: fmt.Sprintf("tab-%d-%d", sessionIndex, tabIndex),
82+ Index: uint16(tabIndex),
83+ Detail: strings.Repeat("<", RemoteCatalogMaxDetailBytes),
84+ }
85+ }
86+ sessions[sessionIndex] = RemoteCatalogSession{
87+ LifecycleID: [16]byte{byte(sessionIndex + 1)},
88+ Name: fmt.Sprintf("work-%d", sessionIndex),
89+ State: RemoteCatalogSessionUp,
90+ Tabs: tabs,
91+ }
92+ }
93+ catalog := RemoteCatalog{
94+ ProtocolVersion: protocol.Version,
95+ SchemaVersion: RemoteCatalogSchemaVersion,
96+ Sessions: sessions,
97+ }
98+ encoded, err := json.Marshal(catalog)
99+ require.NoError(t, err)
100+ require.Greater(t, len(encoded)+1, RemoteCatalogMaxCatalogBytes)
101+ require.ErrorIs(t, ValidateRemoteCatalog(catalog), ErrRemoteCatalogTooLarge)
102+}
103+
72 func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) {104 func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) {
73 err := ValidateRemoteCatalog(RemoteCatalog{105 err := ValidateRemoteCatalog(RemoteCatalog{
74 ProtocolVersion: protocol.Version,106 ProtocolVersion: protocol.Version,

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 12

Tags 11

↑↓ moveenter openesc close