package decoders import ( "unicode/utf8" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "github.com/trufflesecurity/trufflehog/v3/pkg/sources" ) type UTF8 struct{} func (d *UTF8) Type() detectorspb.DecoderType { return detectorspb.DecoderType_PLAIN } func (d *UTF8) FromChunk(chunk *sources.Chunk) *DecodableChunk { if chunk == nil || len(chunk.Data) == 0 { return nil } decodableChunk := &DecodableChunk{Chunk: chunk, DecoderType: d.Type()} if !utf8.Valid(chunk.Data) { chunk.Data = extractSubstrings(chunk.Data) return decodableChunk } return decodableChunk } // utf8ReplacementBytes holds the UTF-8 encoded form of the Unicode replacement character (U+FFFD). // This is pre-computed since it's used frequently when replacing invalid UTF-8 sequences // and control characters. var utf8ReplacementBytes = []byte(string(utf8.RuneError)) // extractSubstrings sanitizes byte sequences to ensure consistent handling of malformed input // while maintaining readable content. It handles ASCII and UTF-8 data as follows: // // For ASCII range (0-127): preserves printable characters (32-126) while replacing // control characters with the UTF-8 replacement character. // https://cs.opensource.google/go/go/+/refs/tags/go1.23.3:src/unicode/utf8/utf8.go;l=16 // // For multi-byte sequences: preserves valid UTF-8 as-is, while invalid sequences // are replaced with a single UTF-8 replacement character. func extractSubstrings(b []byte) []byte { dataLen := len(b) buf := make([]byte, 0, dataLen) for idx := 0; idx < dataLen; { // If it's ASCII, handle separately. // This is faster than decoding for common cases. if b[idx] < utf8.RuneSelf { if isPrintableByte(b[idx]) { buf = append(buf, b[idx]) } else { buf = append(buf, utf8ReplacementBytes...) } idx++ continue } r, size := utf8.DecodeRune(b[idx:]) if r == utf8.RuneError { // Collapse any malformed sequence into a single replacement character // rather than replacing each byte individually. buf = append(buf, utf8ReplacementBytes...) idx++ } else { // Keep valid multi-byte UTF-8 sequences intact to preserve unicode characters. buf = append(buf, b[idx:idx+size]...) idx += size } } return buf } // isPrintableByte reports whether a byte represents a printable ASCII character // using a fast byte-range check. This avoids the overhead of utf8.DecodeRune // for the common case of ASCII characters (0-127), since we know any byte < 128 // represents a complete ASCII character and doesn't need UTF-8 decoding. // This includes letters, digits, punctuation, and symbols, but excludes control characters. // The upper bound is 127 (not 128) because 127 is the DEL control character. // // https://www.rapidtables.com/code/text/ascii-table.html func isPrintableByte(c byte) bool { return c > 31 && c < 127 }