mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
* fix: prevent redactors from corrupting binary files (#102) Redactors were adding newlines to files without them, corrupting binary files during support bundle collection (51 bytes → 53 bytes). Created LineReader to track original newline state and only restore newlines when they were present in the original file. - Added pkg/redact/line_reader.go - Refactored single_line.go, multi_line.go, literal.go - Added 48 tests, all passing - Verified: binary files now preserved byte-for-byte Fixes #102 * fix: handle empty lines correctly in MultiLineRedactor - Check line1 == nil instead of len(line1) == 0 for empty file detection - Fixes edge case where file containing only '\n' would be dropped - Addresses bugbot finding about empty line handling * fix: handle empty lines correctly in MultiLineRedactor - Check line1 != nil instead of len(line1) > 0 in both locations - Fixes edge case where empty trailing lines would be dropped - Fix test isolation in literal_test.go (move ResetRedactionList to parent) - Addresses bugbot findings about empty line handling * fmt * chore: update regression baselines from run 20107431959 * adding defense * fix: propagate non-EOF errors in all early return paths Ensure non-EOF errors (like buffer overflow) are properly propagated to caller in both pre-loop early returns. Addresses bugbot finding. * fix: use unique test names to prevent redaction list pollution Use t.Name() instead of hardcoded 'test' to ensure each test has unique redactor name, preventing parallel test interference --------- Co-authored-by: hedge-sparrow <sparrow@spooky.academy>
157 lines
4.0 KiB
Go
157 lines
4.0 KiB
Go
package redact
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
|
|
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
type SingleLineRedactor struct {
|
|
scan *regexp.Regexp
|
|
re *regexp.Regexp
|
|
maskText string
|
|
filePath string
|
|
redactName string
|
|
isDefault bool
|
|
}
|
|
|
|
var NEW_LINE = []byte{'\n'}
|
|
|
|
func NewSingleLineRedactor(re LineRedactor, maskText, path, name string, isDefault bool) (*SingleLineRedactor, error) {
|
|
var scanCompiled *regexp.Regexp
|
|
compiled, err := compileRegex(re.regex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if re.scan != "" {
|
|
scanCompiled, err = compileRegex(re.scan)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &SingleLineRedactor{scan: scanCompiled, re: compiled, maskText: maskText, filePath: path, redactName: name, isDefault: isDefault}, nil
|
|
}
|
|
|
|
// Redact processes the input reader line-by-line, applying redaction patterns.
|
|
|
|
// Unlike the previous implementation using bufio.Scanner, this now uses LineReader
|
|
// to preserve the exact newline structure of the input file. Lines that originally
|
|
// ended with \n will have \n added back, while lines without \n (like the last line
|
|
// of a file without a trailing newline, or binary files) will not have \n added.
|
|
// This ensures binary files and text files without trailing newlines are not corrupted.
|
|
func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
|
|
out, writer := io.Pipe()
|
|
|
|
go func() {
|
|
var err error
|
|
defer func() {
|
|
if err == nil || err == io.EOF {
|
|
writer.Close()
|
|
} else {
|
|
// Check if error is about line exceeding maximum size
|
|
if errors.Is(err, bufio.ErrTooLong) {
|
|
s := fmt.Sprintf("Error redacting %q. A line in the file exceeded %d MB max length", path, constants.SCANNER_MAX_SIZE/1024/1024)
|
|
klog.V(2).Info(s)
|
|
} else {
|
|
klog.V(2).Info(fmt.Sprintf("Error redacting %q: %v", path, err))
|
|
}
|
|
writer.CloseWithError(err)
|
|
}
|
|
}()
|
|
|
|
// Use LineReader instead of bufio.Scanner to track newline presence
|
|
lineReader := NewLineReader(input)
|
|
tokenizer := GetGlobalTokenizer()
|
|
lineNum := 0
|
|
|
|
for {
|
|
line, hadNewline, readErr := lineReader.ReadLine()
|
|
|
|
// Handle EOF with no content - we're done
|
|
if readErr == io.EOF && len(line) == 0 {
|
|
break
|
|
}
|
|
|
|
// We have content to process
|
|
lineNum++
|
|
|
|
// Determine if we should redact this line
|
|
shouldRedact := true
|
|
|
|
// Pre-filter: if scan is not nil, check if line matches scan by lowercasing it
|
|
if r.scan != nil {
|
|
lowerLine := bytes.ToLower(line)
|
|
if !r.scan.Match(lowerLine) {
|
|
shouldRedact = false
|
|
}
|
|
}
|
|
|
|
// Check if line matches the main redaction pattern
|
|
if shouldRedact && !r.re.Match(line) {
|
|
shouldRedact = false
|
|
}
|
|
|
|
// Process the line (redact or pass through)
|
|
var outputLine []byte
|
|
if shouldRedact {
|
|
// Line matches - perform redaction
|
|
if tokenizer.IsEnabled() {
|
|
// Use tokenized replacement - context comes from the redactor name
|
|
context := r.redactName
|
|
outputLine = getTokenizedReplacementPatternWithPath(r.re, line, context, r.filePath)
|
|
} else {
|
|
// Use original masking behavior
|
|
substStr := []byte(getReplacementPattern(r.re, r.maskText))
|
|
outputLine = r.re.ReplaceAll(line, substStr)
|
|
}
|
|
|
|
// Track redaction if content changed
|
|
if !bytes.Equal(outputLine, line) {
|
|
addRedaction(Redaction{
|
|
RedactorName: r.redactName,
|
|
CharactersRemoved: len(line) - len(outputLine),
|
|
Line: lineNum,
|
|
File: r.filePath,
|
|
IsDefaultRedactor: r.isDefault,
|
|
})
|
|
}
|
|
} else {
|
|
// No match - use original line
|
|
outputLine = line
|
|
}
|
|
|
|
// Write the line
|
|
err = writeBytes(writer, outputLine)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Only add newline if original line had one
|
|
if hadNewline {
|
|
err = writeBytes(writer, NEW_LINE)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check if we hit EOF after processing this line
|
|
if readErr == io.EOF {
|
|
break
|
|
}
|
|
// Check for non-EOF errors
|
|
if readErr != nil {
|
|
err = readErr
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return out
|
|
}
|