package stdcopy import ( "encoding/binary" "errors" "fmt" "io" ) // StdType is the type of standard stream // a writer can multiplex to. type StdType byte const ( Stdin StdType = 0 // Stdin represents standard input stream. It is present for completeness and should NOT be used. When reading the stream with [StdCopy] it is output on [Stdout]. Stdout StdType = 1 // Stdout represents standard output stream. Stderr StdType = 2 // Stderr represents standard error steam. Systemerr StdType = 3 // Systemerr represents errors originating from the system. When reading the stream with [StdCopy] it is returned as an error. ) const ( stdWriterPrefixLen = 8 stdWriterFdIndex = 0 stdWriterSizeIndex = 4 startingBufLen = 32*1024 + stdWriterPrefixLen + 1 ) // StdCopy is a modified version of [io.Copy] to de-multiplex messages // from "multiplexedSource" and copy them to destination streams // "destOut" and "destErr". // // StdCopy demultiplexes "multiplexedSource", assuming that it contains // two streams, previously multiplexed using a writer created with // [NewStdWriter]. // // As it reads from "multiplexedSource", StdCopy writes [Stdout] messages // to "destOut", and [Stderr] message to "destErr]. For backward-compatibility, // [Stdin] messages are output to "destOut". The [Systemerr] stream provides // errors produced by the daemon. It is returned as an error, and terminates // processing the stream. // // StdCopy it reads until it hits [io.EOF] on "multiplexedSource", after // which it returns a nil error. In other words: any error returned indicates // a real underlying error, which may be when an unknown [StdType] stream // is received. // // The "written" return holds the total number of bytes written to "destOut" // and "destErr" combined. func StdCopy(destOut, destErr io.Writer, multiplexedSource io.Reader) (written int64, _ error) { var ( buf = make([]byte, startingBufLen) bufLen = len(buf) nr, nw int err error out io.Writer frameSize int ) for { // Make sure we have at least a full header for nr < stdWriterPrefixLen { var nr2 int nr2, err = multiplexedSource.Read(buf[nr:]) nr += nr2 if errors.Is(err, io.EOF) { if nr < stdWriterPrefixLen { return written, nil } break } if err != nil { return 0, err } } // Check the first byte to know where to write stream := StdType(buf[stdWriterFdIndex]) switch stream { case Stdin: fallthrough case Stdout: // Write on stdout out = destOut case Stderr: // Write on stderr out = destErr case Systemerr: // If we're on Systemerr, we won't write anywhere. // NB: if this code changes later, make sure you don't try to write // to outstream if Systemerr is the stream out = nil default: return 0, fmt.Errorf("unrecognized stream: %d", stream) } // Retrieve the size of the frame frameSize = int(binary.BigEndian.Uint32(buf[stdWriterSizeIndex : stdWriterSizeIndex+4])) // Check if the buffer is big enough to read the frame. // Extend it if necessary. if frameSize+stdWriterPrefixLen > bufLen { buf = append(buf, make([]byte, frameSize+stdWriterPrefixLen-bufLen+1)...) bufLen = len(buf) } // While the amount of bytes read is less than the size of the frame + header, we keep reading for nr < frameSize+stdWriterPrefixLen { var nr2 int nr2, err = multiplexedSource.Read(buf[nr:]) nr += nr2 if errors.Is(err, io.EOF) { if nr < frameSize+stdWriterPrefixLen { return written, nil } break } if err != nil { return 0, err } } // we might have an error from the source mixed up in our multiplexed // stream. if we do, return it. if stream == Systemerr { return written, fmt.Errorf("error from daemon in stream: %s", string(buf[stdWriterPrefixLen:frameSize+stdWriterPrefixLen])) } // Write the retrieved frame (without header) nw, err = out.Write(buf[stdWriterPrefixLen : frameSize+stdWriterPrefixLen]) if err != nil { return 0, err } // If the frame has not been fully written: error if nw != frameSize { return 0, io.ErrShortWrite } written += int64(nw) // Move the rest of the buffer to the beginning copy(buf, buf[frameSize+stdWriterPrefixLen:]) // Move the index nr -= frameSize + stdWriterPrefixLen } }