@@ -26,10 +26,15 @@ func init() {
2626 RootCmd .AddCommand (serveCmd )
2727}
2828
29- // One JSON object per line on stdin. Unknown fields are ignored by
30- // encoding/json by default, giving forward compatibility for free.
29+ // One JSON object per line on stdin, immediately followed - for every
30+ // command, not just render - by a raw "KEY=VALUE\x00" record stream
31+ // terminated by an empty record (a bare NUL); see readEnvBlob. Unknown JSON
32+ // fields are ignored by encoding/json by default, giving forward
33+ // compatibility for free.
3134type serveRequest struct {
32- Env map [string ]string `json:"env"`
35+ // Env is never part of the JSON header - it comes from the raw record
36+ // stream that follows every request line, parsed by readEnvBlob.
37+ Env map [string ]string `json:"-"`
3338 Command string `json:"command"`
3439 Shell string `json:"shell"`
3540 ShellVersion string `json:"shell-version"`
@@ -112,9 +117,11 @@ func createServeCmd() *cmdtree.Command {
112117// primitive fish has) never EOFs the read side. Unix only - the shell owns
113118// the fifo's lifecycle.
114119//
115- // Clients must write each request in a single write(2) call; requests from a
116- // single sequential writer (one shell session) never interleave regardless
117- // of size.
120+ // A request (header line plus its env blob, see readEnvBlob) may span more
121+ // than one write(2) call - the reader is not line/buffer-size bound - but
122+ // those calls must be consecutive with no other writer's bytes landing
123+ // between them. A single sequential writer (one shell session, one request
124+ // at a time) guarantees that regardless of size.
118125func openServeInput (pipePath string ) (* os.File , error ) {
119126 if pipePath == "" {
120127 return os .Stdin , nil
@@ -139,7 +146,8 @@ type serveActiveCycle struct {
139146 copierDone chan struct {}
140147}
141148
142- // runServeLoop reads newline-delimited JSON requests from in and writes
149+ // runServeLoop reads newline-delimited JSON requests (each immediately
150+ // followed by a raw env record blob, see readEnvBlob) from in and writes
143151// NUL-delimited, cycle-id-prefixed prompt records to out. It returns when it
144152// reads a quit command or hits EOF on stdin. The returned bool reports
145153// whether at least one render request was handled, so the caller knows
@@ -152,20 +160,18 @@ type serveActiveCycle struct {
152160// shell additionally redirects this process's stderr so anything unrecovered
153161// can never reach the user's terminal.
154162func runServeLoop (in , out * os.File ) bool {
155- scanner := bufio .NewScanner (in )
156- // Env payloads (a POSH_* overlay plus PATH) can exceed the default 64 KB
157- // scanner buffer, so grow it up front.
158- scanner .Buffer (make ([]byte , 0 , 256 * 1024 ), 1024 * 1024 )
163+ reader := bufio .NewReader (in )
159164
160165 var active * serveActiveCycle
161166 renderedAtLeastOnce := false
162167
163- // envKeys tracks which variables the previous request's overlay set, so
168+ // envKeys tracks which variables the previous request's env blob set, so
164169 // a variable that disappears from a later request (e.g. VIRTUAL_ENV after
165- // `deactivate`) gets unset instead of pinning its stale value for the rest
166- // of the daemon's life. Scoped to the loop so repeated invocations in the
167- // same process (tests) never inherit a previous loop's keys. The serve
168- // loop is single-threaded, so no locking.
170+ // `deactivate`, or anything a client stops forwarding) gets unset instead
171+ // of pinning its stale value for the rest of the daemon's life. Scoped to
172+ // the loop so repeated invocations in the same process (tests) never
173+ // inherit a previous loop's keys. The serve loop is single-threaded, so
174+ // no locking.
169175 envKeys := map [string ]struct {}{}
170176
171177 stopActiveCycle := func () {
@@ -189,52 +195,108 @@ func runServeLoop(in, out *os.File) bool {
189195 active = nil
190196 }
191197
192- for scanner .Scan () {
193- line := scanner .Bytes ()
198+ for {
199+ line , err := reader .ReadBytes ('\n' )
200+ eof := err != nil
194201
202+ line = bytes .TrimSuffix (line , []byte {'\n' })
203+ line = bytes .TrimSuffix (line , []byte {'\r' })
195204 // Strip a UTF-8 BOM: .NET's default UTF8 encoding writes one on the
196205 // StreamWriter's first write, which would otherwise make the first
197206 // request line of a session unparseable.
198207 line = bytes .TrimPrefix (line , []byte {0xEF , 0xBB , 0xBF })
199208
200209 if len (line ) == 0 {
210+ if eof {
211+ break
212+ }
201213 continue
202214 }
203215
204- var req serveRequest
205- if err := json .Unmarshal (line , & req ); err != nil {
206- // Malformed line: ignore for forward/backward compatibility.
207- continue
216+ // A well-formed client always sends the env blob right after the
217+ // header line, for every command - even abort/quit send a bare NUL
218+ // terminator. Reading it here, unconditionally, is what keeps the
219+ // stream in sync regardless of the header's command or JSON validity;
220+ // a client that skipped it on some commands would desync every
221+ // request after the first one that did.
222+ env , envErr := readEnvBlob (reader )
223+ if envErr != nil {
224+ // Truncated/closed mid-blob: nothing more can be recovered.
225+ break
208226 }
209227
210- switch req .Command {
211- case serveCommandRender :
212- // A new render request implicitly aborts whatever is running.
213- stopActiveCycle ()
214- // A nil cycle means setup panicked before prompt.New completed -
215- // template.Init may never have run, in which case the shutdown
216- // path must not call template.SaveCache (it dereferences state
217- // only Init sets). A started cycle implies Init completed.
218- if active = startRenderCycle (& req , out , envKeys ); active != nil {
219- renderedAtLeastOnce = true
228+ var req serveRequest
229+ if err := json .Unmarshal (line , & req ); err == nil {
230+ req .Env = env
231+
232+ switch req .Command {
233+ case serveCommandRender :
234+ // A new render request implicitly aborts whatever is running.
235+ stopActiveCycle ()
236+ // A nil cycle means setup panicked before prompt.New completed -
237+ // template.Init may never have run, in which case the shutdown
238+ // path must not call template.SaveCache (it dereferences state
239+ // only Init sets). A started cycle implies Init completed.
240+ if active = startRenderCycle (& req , out , envKeys ); active != nil {
241+ renderedAtLeastOnce = true
242+ }
243+ case serveCommandAbort :
244+ stopActiveCycle ()
245+ case serveCommandQuit :
246+ stopActiveCycle ()
247+ return renderedAtLeastOnce
248+ default :
249+ // Unknown command: ignore for forward compatibility.
220250 }
221- case serveCommandAbort :
222- stopActiveCycle ()
223- case serveCommandQuit :
224- stopActiveCycle ()
225- return renderedAtLeastOnce
226- default :
227- // Unknown command: ignore for forward compatibility.
251+ }
252+ // Malformed JSON header: ignored for forward/backward compatibility
253+ // (its env blob was already consumed above, keeping the stream in sync).
254+
255+ if eof {
256+ break
228257 }
229258 }
230259
231- // EOF (or a scanner error) on stdin: behave like an explicit quit so
232- // caches are still flushed by the caller's deferred cleanup.
260+ // EOF (or a read error) on stdin: behave like an explicit quit so caches
261+ // are still flushed by the caller's deferred cleanup.
233262 stopActiveCycle ()
234263
235264 return renderedAtLeastOnce
236265}
237266
267+ // readEnvBlob reads a "KEY=VALUE\x00" record stream from r, terminated by an
268+ // empty record (a bare NUL byte). Every request line is unconditionally
269+ // followed by this blob - even for commands that ignore its contents - so
270+ // the reader never needs to know in advance whether one is coming.
271+ //
272+ // Environment variable values cannot contain a NUL byte on any OS this
273+ // project targets (POSIX environ entries and the Windows environment block
274+ // are themselves NUL-terminated/-delimited C strings), so this framing needs
275+ // no escaping: a key/value pair is malformed only if it has no '=', in which
276+ // case it is skipped.
277+ func readEnvBlob (r * bufio.Reader ) (map [string ]string , error ) {
278+ env := map [string ]string {}
279+
280+ for {
281+ record , err := r .ReadBytes (0 )
282+ if err != nil {
283+ return nil , err
284+ }
285+
286+ record = record [:len (record )- 1 ] // drop the trailing NUL delimiter
287+ if len (record ) == 0 {
288+ return env , nil
289+ }
290+
291+ key , value , found := bytes .Cut (record , []byte {'=' })
292+ if ! found {
293+ continue
294+ }
295+
296+ env [string (key )] = string (value )
297+ }
298+ }
299+
238300func applyEnvOverlay (env map [string ]string , keys map [string ]struct {}) {
239301 for key := range keys {
240302 if _ , ok := env [key ]; ok {
0 commit comments