kvm/internal/audio/validation_enhanced.go

480 lines
16 KiB
Go

package audio
import (
"errors"
"fmt"
"time"
"unsafe"
"github.com/rs/zerolog"
)
// Validation errors
var (
ErrInvalidFrameLength = errors.New("invalid frame length")
ErrFrameDataCorrupted = errors.New("frame data appears corrupted")
ErrBufferAlignment = errors.New("buffer alignment invalid")
ErrInvalidSampleFormat = errors.New("invalid sample format")
ErrInvalidTimestamp = errors.New("invalid timestamp")
ErrConfigurationMismatch = errors.New("configuration mismatch")
ErrResourceExhaustion = errors.New("resource exhaustion detected")
ErrInvalidPointer = errors.New("invalid pointer")
ErrBufferOverflow = errors.New("buffer overflow detected")
ErrInvalidState = errors.New("invalid state")
ErrOperationTimeout = errors.New("operation timeout")
ErrSystemOverload = errors.New("system overload detected")
ErrHardwareFailure = errors.New("hardware failure")
ErrNetworkError = errors.New("network error")
ErrMemoryExhaustion = errors.New("memory exhaustion")
)
// ValidationLevel defines the level of validation to perform
type ValidationLevel int
const (
ValidationMinimal ValidationLevel = iota // Only critical safety checks
ValidationStandard // Standard validation for production
ValidationStrict // Comprehensive validation for debugging
)
// ValidationConfig controls validation behavior
type ValidationConfig struct {
Level ValidationLevel
EnableRangeChecks bool
EnableAlignmentCheck bool
EnableDataIntegrity bool
MaxValidationTime time.Duration
}
// GetValidationConfig returns the current validation configuration
func GetValidationConfig() ValidationConfig {
// Use direct config access
config := GetConfig()
return ValidationConfig{
Level: ValidationStandard,
EnableRangeChecks: true,
EnableAlignmentCheck: true,
EnableDataIntegrity: false, // Disabled by default for performance
MaxValidationTime: config.MaxValidationTime, // Configurable validation timeout
}
}
// ValidateAudioFrameFast performs minimal validation for performance-critical paths
func ValidateAudioFrameFast(data []byte) error {
if len(data) == 0 {
return ErrInvalidFrameData
}
// Quick bounds check using direct config access
config := GetConfig()
if len(data) > config.MaxAudioFrameSize {
return fmt.Errorf("%w: frame size %d exceeds maximum %d", ErrInvalidFrameSize, len(data), config.MaxAudioFrameSize)
}
return nil
}
// ValidateAudioFrameComprehensive performs thorough validation
func ValidateAudioFrameComprehensive(data []byte, expectedSampleRate int, expectedChannels int) error {
validationConfig := GetValidationConfig()
start := time.Now()
// Timeout protection for validation
defer func() {
if time.Since(start) > validationConfig.MaxValidationTime {
// Log validation timeout but don't fail
getValidationLogger().Warn().Dur("duration", time.Since(start)).Msg("validation timeout exceeded")
}
}()
// Basic validation first
if err := ValidateAudioFrameFast(data); err != nil {
return err
}
// Range validation
if validationConfig.EnableRangeChecks {
// Use direct config access
config := GetConfig()
if len(data) < config.MinFrameSize {
return fmt.Errorf("%w: frame size %d below minimum %d", ErrInvalidFrameSize, len(data), config.MinFrameSize)
}
// Validate frame length matches expected sample format
expectedFrameSize := (expectedSampleRate * expectedChannels * 2) / 1000 * int(config.AudioQualityMediumFrameSize/time.Millisecond)
if abs(len(data)-expectedFrameSize) > config.FrameSizeTolerance {
return fmt.Errorf("%w: frame size %d doesn't match expected %d (±%d)", ErrInvalidFrameLength, len(data), expectedFrameSize, config.FrameSizeTolerance)
}
}
// Alignment validation for ARM32 compatibility
if validationConfig.EnableAlignmentCheck {
if uintptr(unsafe.Pointer(&data[0]))%4 != 0 {
return fmt.Errorf("%w: buffer not 4-byte aligned for ARM32", ErrBufferAlignment)
}
}
// Data integrity checks (expensive, only for debugging)
if validationConfig.EnableDataIntegrity && validationConfig.Level == ValidationStrict {
if err := validateAudioDataIntegrity(data, expectedChannels); err != nil {
return err
}
}
return nil
}
// ValidateZeroCopyFrameEnhanced performs enhanced zero-copy frame validation
func ValidateZeroCopyFrameEnhanced(frame *ZeroCopyAudioFrame) error {
if frame == nil {
return fmt.Errorf("%w: frame is nil", ErrInvalidPointer)
}
// Check reference count validity
frame.mutex.RLock()
refCount := frame.refCount
length := frame.length
capacity := frame.capacity
frame.mutex.RUnlock()
if refCount <= 0 {
return fmt.Errorf("%w: invalid reference count %d", ErrInvalidState, refCount)
}
if length < 0 || capacity < 0 {
return fmt.Errorf("%w: negative length (%d) or capacity (%d)", ErrInvalidState, length, capacity)
}
if length > capacity {
return fmt.Errorf("%w: length %d exceeds capacity %d", ErrBufferOverflow, length, capacity)
}
// Validate the underlying data
data := frame.Data()
return ValidateAudioFrameFast(data)
}
// ValidateBufferBounds performs bounds checking with overflow protection
func ValidateBufferBounds(buffer []byte, offset, length int) error {
if buffer == nil {
return fmt.Errorf("%w: buffer is nil", ErrInvalidPointer)
}
if offset < 0 {
return fmt.Errorf("%w: negative offset %d", ErrInvalidState, offset)
}
if length < 0 {
return fmt.Errorf("%w: negative length %d", ErrInvalidState, length)
}
// Check for integer overflow
if offset > len(buffer) {
return fmt.Errorf("%w: offset %d exceeds buffer length %d", ErrBufferOverflow, offset, len(buffer))
}
// Safe addition check for overflow
if offset+length < offset || offset+length > len(buffer) {
return fmt.Errorf("%w: range [%d:%d] exceeds buffer length %d", ErrBufferOverflow, offset, offset+length, len(buffer))
}
return nil
}
// ValidateAudioConfiguration performs comprehensive configuration validation
func ValidateAudioConfiguration(config AudioConfig) error {
if err := ValidateAudioQuality(config.Quality); err != nil {
return fmt.Errorf("quality validation failed: %w", err)
}
// Use direct config access
configConstants := GetConfig()
minOpusBitrate := configConstants.MinOpusBitrate
maxOpusBitrate := configConstants.MaxOpusBitrate
maxChannels := configConstants.MaxChannels
validSampleRates := configConstants.ValidSampleRates
minFrameDuration := configConstants.MinFrameDuration
maxFrameDuration := configConstants.MaxFrameDuration
// Validate bitrate ranges
if config.Bitrate < minOpusBitrate || config.Bitrate > maxOpusBitrate {
return fmt.Errorf("%w: bitrate %d outside valid range [%d, %d]", ErrInvalidConfiguration, config.Bitrate, minOpusBitrate, maxOpusBitrate)
}
// Validate sample rate
validSampleRate := false
for _, rate := range validSampleRates {
if config.SampleRate == rate {
validSampleRate = true
break
}
}
if !validSampleRate {
return fmt.Errorf("%w: sample rate %d not in supported rates %v", ErrInvalidSampleRate, config.SampleRate, validSampleRates)
}
// Validate channels
if config.Channels < 1 || config.Channels > maxChannels {
return fmt.Errorf("%w: channels %d outside valid range [1, %d]", ErrInvalidChannels, config.Channels, maxChannels)
}
// Validate frame size
if config.FrameSize < minFrameDuration || config.FrameSize > maxFrameDuration {
return fmt.Errorf("%w: frame size %v outside valid range [%v, %v]", ErrInvalidConfiguration, config.FrameSize, minFrameDuration, maxFrameDuration)
}
return nil
}
// ValidateAudioConfigConstants performs comprehensive validation of AudioConfigConstants
func ValidateAudioConfigConstants(config *AudioConfigConstants) error {
if config == nil {
return fmt.Errorf("%w: configuration is nil", ErrInvalidConfiguration)
}
// Validate basic audio parameters
if config.MaxAudioFrameSize <= 0 {
return fmt.Errorf("%w: MaxAudioFrameSize must be positive", ErrInvalidConfiguration)
}
if config.SampleRate <= 0 {
return fmt.Errorf("%w: SampleRate must be positive", ErrInvalidSampleRate)
}
if config.Channels <= 0 || config.Channels > 8 {
return fmt.Errorf("%w: Channels must be between 1 and 8", ErrInvalidChannels)
}
// Validate Opus parameters
if config.OpusBitrate < 6000 || config.OpusBitrate > 510000 {
return fmt.Errorf("%w: OpusBitrate must be between 6000 and 510000", ErrInvalidConfiguration)
}
if config.OpusComplexity < 0 || config.OpusComplexity > 10 {
return fmt.Errorf("%w: OpusComplexity must be between 0 and 10", ErrInvalidConfiguration)
}
// Validate bitrate ranges
if config.MinOpusBitrate <= 0 || config.MaxOpusBitrate <= 0 {
return fmt.Errorf("%w: MinOpusBitrate and MaxOpusBitrate must be positive", ErrInvalidConfiguration)
}
if config.MinOpusBitrate >= config.MaxOpusBitrate {
return fmt.Errorf("%w: MinOpusBitrate must be less than MaxOpusBitrate", ErrInvalidConfiguration)
}
// Validate sample rate ranges
if config.MinSampleRate <= 0 || config.MaxSampleRate <= 0 {
return fmt.Errorf("%w: MinSampleRate and MaxSampleRate must be positive", ErrInvalidSampleRate)
}
if config.MinSampleRate >= config.MaxSampleRate {
return fmt.Errorf("%w: MinSampleRate must be less than MaxSampleRate", ErrInvalidSampleRate)
}
// Validate frame duration ranges
if config.MinFrameDuration <= 0 || config.MaxFrameDuration <= 0 {
return fmt.Errorf("%w: MinFrameDuration and MaxFrameDuration must be positive", ErrInvalidConfiguration)
}
if config.MinFrameDuration >= config.MaxFrameDuration {
return fmt.Errorf("%w: MinFrameDuration must be less than MaxFrameDuration", ErrInvalidConfiguration)
}
// Validate buffer sizes
if config.SocketMinBuffer <= 0 || config.SocketMaxBuffer <= 0 {
return fmt.Errorf("%w: SocketMinBuffer and SocketMaxBuffer must be positive", ErrInvalidBufferSize)
}
if config.SocketMinBuffer >= config.SocketMaxBuffer {
return fmt.Errorf("%w: SocketMinBuffer must be less than SocketMaxBuffer", ErrInvalidBufferSize)
}
// Validate priority ranges
if config.MinNiceValue < -20 || config.MinNiceValue > 19 {
return fmt.Errorf("%w: MinNiceValue must be between -20 and 19", ErrInvalidPriority)
}
if config.MaxNiceValue < -20 || config.MaxNiceValue > 19 {
return fmt.Errorf("%w: MaxNiceValue must be between -20 and 19", ErrInvalidPriority)
}
if config.MinNiceValue >= config.MaxNiceValue {
return fmt.Errorf("%w: MinNiceValue must be less than MaxNiceValue", ErrInvalidPriority)
}
// Validate timeout values
if config.MaxValidationTime <= 0 {
return fmt.Errorf("%w: MaxValidationTime must be positive", ErrInvalidConfiguration)
}
if config.RestartDelay <= 0 || config.MaxRestartDelay <= 0 {
return fmt.Errorf("%w: RestartDelay and MaxRestartDelay must be positive", ErrInvalidConfiguration)
}
if config.RestartDelay >= config.MaxRestartDelay {
return fmt.Errorf("%w: RestartDelay must be less than MaxRestartDelay", ErrInvalidConfiguration)
}
// Validate valid sample rates array
if len(config.ValidSampleRates) == 0 {
return fmt.Errorf("%w: ValidSampleRates cannot be empty", ErrInvalidSampleRate)
}
for _, rate := range config.ValidSampleRates {
if rate <= 0 {
return fmt.Errorf("%w: all ValidSampleRates must be positive", ErrInvalidSampleRate)
}
if rate < config.MinSampleRate || rate > config.MaxSampleRate {
return fmt.Errorf("%w: ValidSampleRate %d outside range [%d, %d]", ErrInvalidSampleRate, rate, config.MinSampleRate, config.MaxSampleRate)
}
}
return nil
}
// ValidateResourceLimits checks if system resources are within acceptable limits
func ValidateResourceLimits() error {
config := GetConfig()
// Check buffer pool sizes
framePoolStats := GetAudioBufferPoolStats()
if framePoolStats.FramePoolSize > int64(config.MaxPoolSize*2) {
return fmt.Errorf("%w: frame pool size %d exceeds safe limit %d", ErrResourceExhaustion, framePoolStats.FramePoolSize, config.MaxPoolSize*2)
}
// Check zero-copy pool allocation count
zeroCopyStats := GetGlobalZeroCopyPoolStats()
if zeroCopyStats.AllocationCount > int64(config.MaxPoolSize*3) {
return fmt.Errorf("%w: zero-copy allocations %d exceed safe limit %d", ErrResourceExhaustion, zeroCopyStats.AllocationCount, config.MaxPoolSize*3)
}
return nil
}
// validateAudioDataIntegrity performs expensive data integrity checks
func validateAudioDataIntegrity(data []byte, channels int) error {
if len(data)%2 != 0 {
return fmt.Errorf("%w: odd number of bytes for 16-bit samples", ErrInvalidSampleFormat)
}
if len(data)%(channels*2) != 0 {
return fmt.Errorf("%w: data length %d not aligned to channel count %d", ErrInvalidSampleFormat, len(data), channels)
}
// Check for obvious corruption patterns (all zeros, all max values)
sampleCount := len(data) / 2
zeroCount := 0
maxCount := 0
for i := 0; i < len(data); i += 2 {
sample := int16(data[i]) | int16(data[i+1])<<8
switch sample {
case 0:
zeroCount++
case 32767, -32768:
maxCount++
}
}
// Flag suspicious patterns
if zeroCount > sampleCount*9/10 {
return fmt.Errorf("%w: %d%% zero samples suggests silence or corruption", ErrFrameDataCorrupted, (zeroCount*100)/sampleCount)
}
if maxCount > sampleCount/10 {
return fmt.Errorf("%w: %d%% max-value samples suggests clipping or corruption", ErrFrameDataCorrupted, (maxCount*100)/sampleCount)
}
return nil
}
// Helper function for absolute value
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
// getValidationLogger returns a logger for validation operations
func getValidationLogger() *zerolog.Logger {
// Return a basic logger for validation
logger := zerolog.New(nil).With().Timestamp().Logger()
return &logger
}
// ErrorContext provides structured error context for better debugging
type ErrorContext struct {
Component string `json:"component"`
Operation string `json:"operation"`
Timestamp time.Time `json:"timestamp"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
StackTrace []string `json:"stack_trace,omitempty"`
}
// ContextualError wraps an error with additional context
type ContextualError struct {
Err error `json:"error"`
Context ErrorContext `json:"context"`
}
func (ce *ContextualError) Error() string {
return fmt.Sprintf("%s [%s:%s]: %v", ce.Context.Component, ce.Context.Operation, ce.Context.Timestamp.Format(time.RFC3339), ce.Err)
}
func (ce *ContextualError) Unwrap() error {
return ce.Err
}
// NewContextualError creates a new contextual error with metadata
func NewContextualError(err error, component, operation string, metadata map[string]interface{}) *ContextualError {
return &ContextualError{
Err: err,
Context: ErrorContext{
Component: component,
Operation: operation,
Timestamp: time.Now(),
Metadata: metadata,
},
}
}
// WrapWithContext wraps an error with component and operation context
func WrapWithContext(err error, component, operation string) error {
if err == nil {
return nil
}
return NewContextualError(err, component, operation, nil)
}
// WrapWithMetadata wraps an error with additional metadata
func WrapWithMetadata(err error, component, operation string, metadata map[string]interface{}) error {
if err == nil {
return nil
}
return NewContextualError(err, component, operation, metadata)
}
// IsTimeoutError checks if an error is a timeout error
func IsTimeoutError(err error) bool {
return errors.Is(err, ErrOperationTimeout)
}
// IsResourceError checks if an error is related to resource exhaustion
func IsResourceError(err error) bool {
return errors.Is(err, ErrResourceExhaustion) || errors.Is(err, ErrMemoryExhaustion) || errors.Is(err, ErrSystemOverload)
}
// IsHardwareError checks if an error is hardware-related
func IsHardwareError(err error) bool {
return errors.Is(err, ErrHardwareFailure)
}
// IsNetworkError checks if an error is network-related
func IsNetworkError(err error) bool {
return errors.Is(err, ErrNetworkError)
}
// GetErrorSeverity returns the severity level of an error
func GetErrorSeverity(err error) string {
if IsHardwareError(err) {
return "critical"
}
if IsResourceError(err) {
return "high"
}
if IsNetworkError(err) || IsTimeoutError(err) {
return "medium"
}
return "low"
}