mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
195 lines
3.8 KiB
Go
195 lines
3.8 KiB
Go
// Package sanitize implements a streaming PTY output sanitizer.
|
||
//
|
||
// It strips ANSI escape sequences (CSI, OSC), normalizes carriage-return
|
||
// progress updates into the final visible line, and performs incremental
|
||
// UTF-8 decoding—mirroring the Python shellctl_runtime/sanitize.py.
|
||
package sanitize
|
||
|
||
import (
|
||
"bufio"
|
||
"io"
|
||
"os"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
// escapeState tracks the ANSI escape sequence parser state.
|
||
type escapeState int
|
||
|
||
const (
|
||
stateNormal escapeState = iota
|
||
stateEsc
|
||
stateCSI
|
||
stateOSC
|
||
stateOSCEsc
|
||
)
|
||
|
||
// PtySanitizer incrementally converts PTY bytes into stable, readable UTF-8.
|
||
type PtySanitizer struct {
|
||
lineBuffer []byte
|
||
pendingCR bool
|
||
state escapeState
|
||
}
|
||
|
||
// New returns a fresh PtySanitizer.
|
||
func New() *PtySanitizer {
|
||
return &PtySanitizer{}
|
||
}
|
||
|
||
// Feed consumes one chunk of decoded text and returns newly stable output.
|
||
func (s *PtySanitizer) Feed(text []byte) []byte {
|
||
var out []byte
|
||
for len(text) > 0 {
|
||
r, size := utf8.DecodeRune(text)
|
||
if r == utf8.RuneError && size <= 1 {
|
||
// Replace invalid byte with U+FFFD
|
||
text = text[1:]
|
||
r = utf8.RuneError
|
||
} else {
|
||
text = text[size:]
|
||
}
|
||
out = s.consumeRune(r, out)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Flush returns any remaining buffered content at end-of-stream.
|
||
func (s *PtySanitizer) Flush() []byte {
|
||
s.state = stateNormal
|
||
s.pendingCR = false
|
||
result := s.lineBuffer
|
||
s.lineBuffer = nil
|
||
return result
|
||
}
|
||
|
||
func (s *PtySanitizer) consumeRune(r rune, out []byte) []byte {
|
||
switch s.state {
|
||
case stateNormal:
|
||
if r == '\x1b' {
|
||
s.state = stateEsc
|
||
return out
|
||
}
|
||
return s.consumeVisible(r, out)
|
||
|
||
case stateEsc:
|
||
switch r {
|
||
case '[':
|
||
s.state = stateCSI
|
||
case ']':
|
||
s.state = stateOSC
|
||
default:
|
||
s.state = stateNormal
|
||
if r != '\x1b' && isPrintable(r) {
|
||
return s.consumeVisible(r, out)
|
||
}
|
||
}
|
||
return out
|
||
|
||
case stateCSI:
|
||
// CSI sequence ends at a byte in the range 0x40–0x7E
|
||
if r >= '@' && r <= '~' {
|
||
s.state = stateNormal
|
||
}
|
||
return out
|
||
|
||
case stateOSC:
|
||
switch r {
|
||
case '\x07':
|
||
s.state = stateNormal
|
||
case '\x1b':
|
||
s.state = stateOSCEsc
|
||
}
|
||
return out
|
||
|
||
case stateOSCEsc:
|
||
if r == '\\' {
|
||
s.state = stateNormal
|
||
} else {
|
||
s.state = stateOSC
|
||
}
|
||
return out
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (s *PtySanitizer) consumeVisible(r rune, out []byte) []byte {
|
||
if s.pendingCR {
|
||
if r == '\n' {
|
||
out = append(out, s.lineBuffer...)
|
||
out = append(out, '\n')
|
||
s.lineBuffer = nil
|
||
s.pendingCR = false
|
||
return out
|
||
}
|
||
// CR without LF: overwrite line buffer (progress update)
|
||
s.lineBuffer = nil
|
||
s.pendingCR = false
|
||
}
|
||
|
||
if r == '\r' {
|
||
s.pendingCR = true
|
||
return out
|
||
}
|
||
if r == '\n' {
|
||
out = append(out, s.lineBuffer...)
|
||
out = append(out, '\n')
|
||
s.lineBuffer = nil
|
||
return out
|
||
}
|
||
|
||
// Append rune to line buffer
|
||
var buf [utf8.UTFMax]byte
|
||
n := utf8.EncodeRune(buf[:], r)
|
||
s.lineBuffer = append(s.lineBuffer, buf[:n]...)
|
||
return out
|
||
}
|
||
|
||
func isPrintable(r rune) bool {
|
||
// Match Python's str.isprintable: exclude C0/C1 control chars
|
||
return r >= 0x20 && r != 0x7f
|
||
}
|
||
|
||
// Run executes the streaming sanitizer as a stdin→stdout filter.
|
||
// If readyFile is non-empty, it is touched before reading begins.
|
||
func Run(readyFile string, stdin io.Reader, stdout io.Writer) error {
|
||
if readyFile != "" {
|
||
f, err := os.Create(readyFile)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
_ = f.Close()
|
||
}
|
||
|
||
sanitizer := New()
|
||
reader := bufio.NewReaderSize(stdin, 65536)
|
||
writer := bufio.NewWriter(stdout)
|
||
defer func() { _ = writer.Flush() }()
|
||
|
||
buf := make([]byte, 65536)
|
||
for {
|
||
n, err := reader.Read(buf)
|
||
if n > 0 {
|
||
out := sanitizer.Feed(buf[:n])
|
||
if len(out) > 0 {
|
||
if _, werr := writer.Write(out); werr != nil {
|
||
return werr
|
||
}
|
||
_ = writer.Flush()
|
||
}
|
||
}
|
||
if err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
return err
|
||
}
|
||
}
|
||
|
||
tail := sanitizer.Flush()
|
||
if len(tail) > 0 {
|
||
if _, err := writer.Write(tail); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return writer.Flush()
|
||
}
|