asset_validator.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211
  1. """
  2. Asset Validation and Integrity Checking for Trixy Application
  3. This module provides comprehensive validation and integrity checking for all
  4. types of assets in the Trixy asset management system. It ensures asset quality,
  5. format compliance, and data integrity.
  6. Features:
  7. - Format-specific validation for all supported asset types
  8. - File integrity checking using checksums and signatures
  9. - Content validation (e.g., JSON syntax, image corruption)
  10. - Size and constraint validation
  11. - Security scanning for malicious content
  12. - Custom validation rule support
  13. - Batch validation for multiple assets
  14. - Validation result reporting and logging
  15. Asset Validation Types:
  16. - Audio files: Format validity, sample rate, channel count, duration
  17. - Image files: Format validity, dimensions, color depth, corruption detection
  18. - Text files: Encoding validation, syntax checking, content analysis
  19. - Config files: Schema validation, format compliance, required fields
  20. - Binary files: Signature verification, integrity checks
  21. - Model files: Format validation, metadata verification
  22. Usage:
  23. from trixy_core.assets.asset_validator import AssetValidator
  24. validator = AssetValidator()
  25. result = validator.validate_asset("/path/to/asset.wav", AssetType.AUDIO_WAV)
  26. if result.is_valid:
  27. print("Asset is valid")
  28. else:
  29. print(f"Validation errors: {result.errors}")
  30. """
  31. import os
  32. import hashlib
  33. import json
  34. import struct
  35. import mimetypes
  36. from pathlib import Path
  37. from typing import Dict, Any, Optional, List, Set, Union, Tuple, Callable
  38. from dataclasses import dataclass, field
  39. from enum import Enum
  40. import re
  41. import time
  42. # Try to import optional dependencies
  43. try:
  44. import yaml
  45. HAS_YAML = True
  46. except ImportError:
  47. HAS_YAML = False
  48. try:
  49. from PIL import Image
  50. HAS_PIL = True
  51. except ImportError:
  52. HAS_PIL = False
  53. # Import asset types (with fallback if not available)
  54. try:
  55. from .asset_types import AssetType, AssetConstraints, get_asset_constraints, ASSET_TYPE_CONSTRAINTS
  56. except ImportError:
  57. # Define minimal types for standalone operation
  58. class AssetType:
  59. AUDIO_WAV = "audio/wav"
  60. IMAGE_PNG = "image/png"
  61. TEXT_JSON = "text/json"
  62. UNKNOWN = "unknown"
  63. ASSET_TYPE_CONSTRAINTS = {}
  64. def get_asset_constraints(asset_type):
  65. return None
  66. class ValidationSeverity(Enum):
  67. """Severity levels for validation issues."""
  68. INFO = "info"
  69. WARNING = "warning"
  70. ERROR = "error"
  71. CRITICAL = "critical"
  72. class ValidationCategory(Enum):
  73. """Categories of validation checks."""
  74. FORMAT = "format"
  75. INTEGRITY = "integrity"
  76. CONTENT = "content"
  77. SECURITY = "security"
  78. PERFORMANCE = "performance"
  79. METADATA = "metadata"
  80. @dataclass
  81. class ValidationIssue:
  82. """Represents a single validation issue."""
  83. category: ValidationCategory
  84. severity: ValidationSeverity
  85. message: str
  86. code: Optional[str] = None
  87. location: Optional[str] = None # Line number, byte offset, etc.
  88. suggestion: Optional[str] = None # How to fix the issue
  89. def __str__(self) -> str:
  90. parts = [f"[{self.severity.value.upper()}]"]
  91. if self.category:
  92. parts.append(f"[{self.category.value}]")
  93. parts.append(self.message)
  94. if self.location:
  95. parts.append(f"at {self.location}")
  96. return " ".join(parts)
  97. @dataclass
  98. class AssetValidationResult:
  99. """Results of asset validation."""
  100. asset_path: str
  101. asset_type: Optional[str] = None
  102. is_valid: bool = True
  103. validation_time: float = 0.0
  104. file_size: int = 0
  105. checksum: Optional[str] = None
  106. # Issues found during validation
  107. issues: List[ValidationIssue] = field(default_factory=list)
  108. # Extracted metadata
  109. metadata: Dict[str, Any] = field(default_factory=dict)
  110. # Performance metrics
  111. load_time: Optional[float] = None
  112. parse_time: Optional[float] = None
  113. @property
  114. def errors(self) -> List[ValidationIssue]:
  115. """Get all error-level issues."""
  116. return [issue for issue in self.issues if issue.severity in (ValidationSeverity.ERROR, ValidationSeverity.CRITICAL)]
  117. @property
  118. def warnings(self) -> List[ValidationIssue]:
  119. """Get all warning-level issues."""
  120. return [issue for issue in self.issues if issue.severity == ValidationSeverity.WARNING]
  121. @property
  122. def has_errors(self) -> bool:
  123. """Check if validation has any errors."""
  124. return len(self.errors) > 0
  125. @property
  126. def has_warnings(self) -> bool:
  127. """Check if validation has any warnings."""
  128. return len(self.warnings) > 0
  129. def add_issue(self, category: ValidationCategory, severity: ValidationSeverity,
  130. message: str, code: str = None, location: str = None, suggestion: str = None):
  131. """Add a validation issue."""
  132. issue = ValidationIssue(
  133. category=category,
  134. severity=severity,
  135. message=message,
  136. code=code,
  137. location=location,
  138. suggestion=suggestion
  139. )
  140. self.issues.append(issue)
  141. # Update is_valid flag
  142. if severity in (ValidationSeverity.ERROR, ValidationSeverity.CRITICAL):
  143. self.is_valid = False
  144. def to_dict(self) -> Dict[str, Any]:
  145. """Convert result to dictionary."""
  146. return {
  147. 'asset_path': self.asset_path,
  148. 'asset_type': self.asset_type,
  149. 'is_valid': self.is_valid,
  150. 'validation_time': self.validation_time,
  151. 'file_size': self.file_size,
  152. 'checksum': self.checksum,
  153. 'issues': [
  154. {
  155. 'category': issue.category.value,
  156. 'severity': issue.severity.value,
  157. 'message': issue.message,
  158. 'code': issue.code,
  159. 'location': issue.location,
  160. 'suggestion': issue.suggestion
  161. }
  162. for issue in self.issues
  163. ],
  164. 'metadata': self.metadata,
  165. 'load_time': self.load_time,
  166. 'parse_time': self.parse_time,
  167. 'error_count': len(self.errors),
  168. 'warning_count': len(self.warnings)
  169. }
  170. def pprint(message: str) -> None:
  171. """Asset validator logging function."""
  172. print(f"[ASSET_VALIDATOR] {message}")
  173. class AssetValidator:
  174. """
  175. Comprehensive asset validation system.
  176. Provides format-specific validation, integrity checking, and content analysis
  177. for all supported asset types in the Trixy system.
  178. """
  179. def __init__(self, strict_mode: bool = False, enable_security_checks: bool = True):
  180. """
  181. Initialize the asset validator.
  182. Args:
  183. strict_mode: Whether to treat warnings as errors
  184. enable_security_checks: Whether to enable security scanning
  185. """
  186. self.strict_mode = strict_mode
  187. self.enable_security_checks = enable_security_checks
  188. # Register built-in validators
  189. self._validators: Dict[str, Callable] = {}
  190. self._register_built_in_validators()
  191. # Security patterns for basic scanning
  192. self._security_patterns = self._compile_security_patterns()
  193. pprint(f"AssetValidator initialized (strict_mode: {strict_mode}, security_checks: {enable_security_checks})")
  194. def _register_built_in_validators(self):
  195. """Register built-in format validators."""
  196. self._validators.update({
  197. 'audio/wav': self._validate_wav_audio,
  198. 'audio/mp3': self._validate_mp3_audio,
  199. 'audio/ogg': self._validate_ogg_audio,
  200. 'image/png': self._validate_png_image,
  201. 'image/jpeg': self._validate_jpeg_image,
  202. 'image/gif': self._validate_gif_image,
  203. 'text/json': self._validate_json_text,
  204. 'text/yaml': self._validate_yaml_text,
  205. 'text/plain': self._validate_plain_text,
  206. 'config/json': self._validate_json_config,
  207. 'config/yaml': self._validate_yaml_config,
  208. 'model/pytorch': self._validate_pytorch_model,
  209. 'script/python': self._validate_python_script,
  210. })
  211. def _compile_security_patterns(self) -> Dict[str, List[re.Pattern]]:
  212. """Compile security scan patterns."""
  213. return {
  214. 'script_injection': [
  215. re.compile(r'<script[^>]*>.*?</script>', re.IGNORECASE | re.DOTALL),
  216. re.compile(r'javascript:', re.IGNORECASE),
  217. re.compile(r'on\w+\s*=', re.IGNORECASE),
  218. ],
  219. 'file_inclusion': [
  220. re.compile(r'\.\./.*\.\./'),
  221. re.compile(r'file://'),
  222. re.compile(r'\\\\.*\\\\'),
  223. ],
  224. 'command_injection': [
  225. re.compile(r'[;&|`$]'),
  226. re.compile(r'eval\s*\('),
  227. re.compile(r'exec\s*\('),
  228. ]
  229. }
  230. def validate_asset(self, file_path: str, asset_type: str = None) -> AssetValidationResult:
  231. """
  232. Validate an asset file comprehensively.
  233. Args:
  234. file_path: Path to the asset file
  235. asset_type: Asset type (will be detected if None)
  236. Returns:
  237. AssetValidationResult with validation details
  238. """
  239. start_time = time.time()
  240. result = AssetValidationResult(
  241. asset_path=file_path,
  242. asset_type=asset_type
  243. )
  244. try:
  245. # Basic file validation
  246. if not self._validate_file_exists(file_path, result):
  247. result.validation_time = time.time() - start_time
  248. return result
  249. # Get file information
  250. path = Path(file_path)
  251. result.file_size = path.stat().st_size
  252. # Calculate checksum
  253. result.checksum = self._calculate_checksum(file_path)
  254. # Detect asset type if not provided
  255. if not asset_type:
  256. asset_type = self._detect_asset_type(file_path)
  257. result.asset_type = asset_type
  258. # Validate constraints
  259. self._validate_constraints(file_path, asset_type, result)
  260. # Format-specific validation
  261. if asset_type in self._validators:
  262. self._validators[asset_type](file_path, result)
  263. else:
  264. self._validate_generic_file(file_path, result)
  265. # Security checks
  266. if self.enable_security_checks:
  267. self._perform_security_checks(file_path, asset_type, result)
  268. # Performance analysis
  269. self._analyze_performance_characteristics(file_path, asset_type, result)
  270. except Exception as e:
  271. result.add_issue(
  272. ValidationCategory.INTEGRITY,
  273. ValidationSeverity.CRITICAL,
  274. f"Validation failed with error: {str(e)}",
  275. code="VALIDATION_ERROR"
  276. )
  277. result.validation_time = time.time() - start_time
  278. # Apply strict mode
  279. if self.strict_mode and result.has_warnings:
  280. result.is_valid = False
  281. return result
  282. def _validate_file_exists(self, file_path: str, result: AssetValidationResult) -> bool:
  283. """Validate that file exists and is accessible."""
  284. path = Path(file_path)
  285. if not path.exists():
  286. result.add_issue(
  287. ValidationCategory.INTEGRITY,
  288. ValidationSeverity.CRITICAL,
  289. "File does not exist",
  290. code="FILE_NOT_FOUND"
  291. )
  292. return False
  293. if not path.is_file():
  294. result.add_issue(
  295. ValidationCategory.INTEGRITY,
  296. ValidationSeverity.ERROR,
  297. "Path is not a regular file",
  298. code="NOT_A_FILE"
  299. )
  300. return False
  301. try:
  302. # Test file accessibility
  303. with open(file_path, 'rb') as f:
  304. f.read(1) # Try to read at least one byte
  305. except PermissionError:
  306. result.add_issue(
  307. ValidationCategory.INTEGRITY,
  308. ValidationSeverity.ERROR,
  309. "File is not readable (permission denied)",
  310. code="PERMISSION_DENIED"
  311. )
  312. return False
  313. except Exception as e:
  314. result.add_issue(
  315. ValidationCategory.INTEGRITY,
  316. ValidationSeverity.ERROR,
  317. f"File access error: {str(e)}",
  318. code="FILE_ACCESS_ERROR"
  319. )
  320. return False
  321. return True
  322. def _calculate_checksum(self, file_path: str) -> str:
  323. """Calculate SHA256 checksum of the file."""
  324. hash_sha256 = hashlib.sha256()
  325. with open(file_path, 'rb') as f:
  326. for chunk in iter(lambda: f.read(4096), b""):
  327. hash_sha256.update(chunk)
  328. return hash_sha256.hexdigest()
  329. def _detect_asset_type(self, file_path: str) -> str:
  330. """Detect asset type from file path and content."""
  331. # This would use the asset_types module if available
  332. # For now, use basic detection
  333. path = Path(file_path)
  334. extension = path.suffix.lower()
  335. extension_map = {
  336. '.wav': 'audio/wav',
  337. '.mp3': 'audio/mp3',
  338. '.png': 'image/png',
  339. '.jpg': 'image/jpeg',
  340. '.jpeg': 'image/jpeg',
  341. '.json': 'text/json',
  342. '.yaml': 'text/yaml',
  343. '.yml': 'text/yaml',
  344. '.txt': 'text/plain'
  345. }
  346. return extension_map.get(extension, 'unknown')
  347. def _validate_constraints(self, file_path: str, asset_type: str, result: AssetValidationResult):
  348. """Validate asset against its type constraints."""
  349. constraints = get_asset_constraints(asset_type) if get_asset_constraints else None
  350. if not constraints:
  351. return
  352. file_size = result.file_size
  353. # Check size limits
  354. if constraints.max_size_bytes and file_size > constraints.max_size_bytes:
  355. result.add_issue(
  356. ValidationCategory.CONTENT,
  357. ValidationSeverity.ERROR,
  358. f"File size ({file_size} bytes) exceeds maximum ({constraints.max_size_bytes} bytes)",
  359. code="FILE_TOO_LARGE",
  360. suggestion=f"Reduce file size to under {constraints.max_size_bytes} bytes"
  361. )
  362. if constraints.min_size_bytes and file_size < constraints.min_size_bytes:
  363. result.add_issue(
  364. ValidationCategory.CONTENT,
  365. ValidationSeverity.ERROR,
  366. f"File size ({file_size} bytes) is below minimum ({constraints.min_size_bytes} bytes)",
  367. code="FILE_TOO_SMALL"
  368. )
  369. # Check file extension
  370. if constraints.allowed_extensions:
  371. path = Path(file_path)
  372. extension = path.suffix.lower()
  373. if extension not in constraints.allowed_extensions:
  374. result.add_issue(
  375. ValidationCategory.FORMAT,
  376. ValidationSeverity.WARNING,
  377. f"File extension '{extension}' not in allowed list: {list(constraints.allowed_extensions)}",
  378. code="UNEXPECTED_EXTENSION"
  379. )
  380. def _validate_wav_audio(self, file_path: str, result: AssetValidationResult):
  381. """Validate WAV audio file."""
  382. try:
  383. with open(file_path, 'rb') as f:
  384. header = f.read(44) # WAV header is typically 44 bytes
  385. if len(header) < 44:
  386. result.add_issue(
  387. ValidationCategory.FORMAT,
  388. ValidationSeverity.ERROR,
  389. "WAV file header too short",
  390. code="WAV_HEADER_TOO_SHORT"
  391. )
  392. return
  393. # Check RIFF signature
  394. if not header.startswith(b'RIFF'):
  395. result.add_issue(
  396. ValidationCategory.FORMAT,
  397. ValidationSeverity.ERROR,
  398. "Invalid WAV file (missing RIFF signature)",
  399. code="WAV_INVALID_SIGNATURE"
  400. )
  401. return
  402. # Check WAVE format
  403. if b'WAVE' not in header[:16]:
  404. result.add_issue(
  405. ValidationCategory.FORMAT,
  406. ValidationSeverity.ERROR,
  407. "Invalid WAV file (missing WAVE format)",
  408. code="WAV_INVALID_FORMAT"
  409. )
  410. return
  411. # Extract audio parameters
  412. try:
  413. # Audio format (2 bytes at offset 20)
  414. audio_format = struct.unpack('<H', header[20:22])[0]
  415. # Number of channels (2 bytes at offset 22)
  416. channels = struct.unpack('<H', header[22:24])[0]
  417. # Sample rate (4 bytes at offset 24)
  418. sample_rate = struct.unpack('<L', header[24:28])[0]
  419. # Bits per sample (2 bytes at offset 34)
  420. bits_per_sample = struct.unpack('<H', header[34:36])[0]
  421. # Store metadata
  422. result.metadata.update({
  423. 'audio_format': audio_format,
  424. 'channels': channels,
  425. 'sample_rate': sample_rate,
  426. 'bits_per_sample': bits_per_sample
  427. })
  428. # Validate parameters
  429. if channels < 1 or channels > 8:
  430. result.add_issue(
  431. ValidationCategory.CONTENT,
  432. ValidationSeverity.WARNING,
  433. f"Unusual channel count: {channels}",
  434. code="WAV_UNUSUAL_CHANNELS"
  435. )
  436. if sample_rate < 8000 or sample_rate > 192000:
  437. result.add_issue(
  438. ValidationCategory.CONTENT,
  439. ValidationSeverity.WARNING,
  440. f"Unusual sample rate: {sample_rate} Hz",
  441. code="WAV_UNUSUAL_SAMPLE_RATE"
  442. )
  443. if bits_per_sample not in [8, 16, 24, 32]:
  444. result.add_issue(
  445. ValidationCategory.CONTENT,
  446. ValidationSeverity.WARNING,
  447. f"Unusual bit depth: {bits_per_sample} bits",
  448. code="WAV_UNUSUAL_BIT_DEPTH"
  449. )
  450. except struct.error as e:
  451. result.add_issue(
  452. ValidationCategory.FORMAT,
  453. ValidationSeverity.ERROR,
  454. f"Error parsing WAV header: {str(e)}",
  455. code="WAV_HEADER_PARSE_ERROR"
  456. )
  457. except Exception as e:
  458. result.add_issue(
  459. ValidationCategory.INTEGRITY,
  460. ValidationSeverity.ERROR,
  461. f"Error validating WAV file: {str(e)}",
  462. code="WAV_VALIDATION_ERROR"
  463. )
  464. def _validate_mp3_audio(self, file_path: str, result: AssetValidationResult):
  465. """Validate MP3 audio file."""
  466. try:
  467. with open(file_path, 'rb') as f:
  468. header = f.read(10) # Read first 10 bytes
  469. # Check for ID3 tag or MPEG sync
  470. if header.startswith(b'ID3'):
  471. result.metadata['has_id3_tag'] = True
  472. elif header.startswith((b'\xFF\xFB', b'\xFF\xFA')):
  473. result.metadata['has_mpeg_sync'] = True
  474. else:
  475. result.add_issue(
  476. ValidationCategory.FORMAT,
  477. ValidationSeverity.WARNING,
  478. "MP3 file missing expected signature",
  479. code="MP3_MISSING_SIGNATURE"
  480. )
  481. except Exception as e:
  482. result.add_issue(
  483. ValidationCategory.INTEGRITY,
  484. ValidationSeverity.ERROR,
  485. f"Error validating MP3 file: {str(e)}",
  486. code="MP3_VALIDATION_ERROR"
  487. )
  488. def _validate_ogg_audio(self, file_path: str, result: AssetValidationResult):
  489. """Validate OGG audio file."""
  490. try:
  491. with open(file_path, 'rb') as f:
  492. header = f.read(4)
  493. if not header.startswith(b'OggS'):
  494. result.add_issue(
  495. ValidationCategory.FORMAT,
  496. ValidationSeverity.ERROR,
  497. "Invalid OGG file (missing OggS signature)",
  498. code="OGG_INVALID_SIGNATURE"
  499. )
  500. except Exception as e:
  501. result.add_issue(
  502. ValidationCategory.INTEGRITY,
  503. ValidationSeverity.ERROR,
  504. f"Error validating OGG file: {str(e)}",
  505. code="OGG_VALIDATION_ERROR"
  506. )
  507. def _validate_png_image(self, file_path: str, result: AssetValidationResult):
  508. """Validate PNG image file."""
  509. try:
  510. with open(file_path, 'rb') as f:
  511. header = f.read(24) # PNG signature + IHDR chunk start
  512. # Check PNG signature
  513. if not header.startswith(b'\x89PNG\r\n\x1a\n'):
  514. result.add_issue(
  515. ValidationCategory.FORMAT,
  516. ValidationSeverity.ERROR,
  517. "Invalid PNG file signature",
  518. code="PNG_INVALID_SIGNATURE"
  519. )
  520. return
  521. # Extract image dimensions
  522. if len(header) >= 24:
  523. width = struct.unpack('>I', header[16:20])[0]
  524. height = struct.unpack('>I', header[20:24])[0]
  525. result.metadata.update({
  526. 'width': width,
  527. 'height': height,
  528. 'aspect_ratio': width / height if height > 0 else 0
  529. })
  530. # Validate dimensions
  531. if width == 0 or height == 0:
  532. result.add_issue(
  533. ValidationCategory.CONTENT,
  534. ValidationSeverity.ERROR,
  535. f"Invalid image dimensions: {width}x{height}",
  536. code="PNG_INVALID_DIMENSIONS"
  537. )
  538. if width > 16384 or height > 16384:
  539. result.add_issue(
  540. ValidationCategory.PERFORMANCE,
  541. ValidationSeverity.WARNING,
  542. f"Very large image dimensions: {width}x{height}",
  543. code="PNG_LARGE_DIMENSIONS",
  544. suggestion="Consider reducing image size for better performance"
  545. )
  546. except Exception as e:
  547. result.add_issue(
  548. ValidationCategory.INTEGRITY,
  549. ValidationSeverity.ERROR,
  550. f"Error validating PNG file: {str(e)}",
  551. code="PNG_VALIDATION_ERROR"
  552. )
  553. def _validate_jpeg_image(self, file_path: str, result: AssetValidationResult):
  554. """Validate JPEG image file."""
  555. try:
  556. with open(file_path, 'rb') as f:
  557. header = f.read(10)
  558. if not header.startswith(b'\xFF\xD8\xFF'):
  559. result.add_issue(
  560. ValidationCategory.FORMAT,
  561. ValidationSeverity.ERROR,
  562. "Invalid JPEG file signature",
  563. code="JPEG_INVALID_SIGNATURE"
  564. )
  565. except Exception as e:
  566. result.add_issue(
  567. ValidationCategory.INTEGRITY,
  568. ValidationSeverity.ERROR,
  569. f"Error validating JPEG file: {str(e)}",
  570. code="JPEG_VALIDATION_ERROR"
  571. )
  572. def _validate_gif_image(self, file_path: str, result: AssetValidationResult):
  573. """Validate GIF image file."""
  574. try:
  575. with open(file_path, 'rb') as f:
  576. header = f.read(10)
  577. if not header.startswith((b'GIF87a', b'GIF89a')):
  578. result.add_issue(
  579. ValidationCategory.FORMAT,
  580. ValidationSeverity.ERROR,
  581. "Invalid GIF file signature",
  582. code="GIF_INVALID_SIGNATURE"
  583. )
  584. except Exception as e:
  585. result.add_issue(
  586. ValidationCategory.INTEGRITY,
  587. ValidationSeverity.ERROR,
  588. f"Error validating GIF file: {str(e)}",
  589. code="GIF_VALIDATION_ERROR"
  590. )
  591. def _validate_json_text(self, file_path: str, result: AssetValidationResult):
  592. """Validate JSON text file."""
  593. try:
  594. with open(file_path, 'r', encoding='utf-8') as f:
  595. content = f.read()
  596. try:
  597. data = json.loads(content)
  598. result.metadata.update({
  599. 'json_valid': True,
  600. 'json_type': type(data).__name__,
  601. 'character_count': len(content)
  602. })
  603. if isinstance(data, dict):
  604. result.metadata['key_count'] = len(data)
  605. elif isinstance(data, list):
  606. result.metadata['item_count'] = len(data)
  607. except json.JSONDecodeError as e:
  608. result.add_issue(
  609. ValidationCategory.CONTENT,
  610. ValidationSeverity.ERROR,
  611. f"Invalid JSON syntax: {str(e)}",
  612. code="JSON_SYNTAX_ERROR",
  613. location=f"line {e.lineno}, column {e.colno}"
  614. )
  615. except UnicodeDecodeError as e:
  616. result.add_issue(
  617. ValidationCategory.FORMAT,
  618. ValidationSeverity.ERROR,
  619. f"Text encoding error: {str(e)}",
  620. code="TEXT_ENCODING_ERROR"
  621. )
  622. except Exception as e:
  623. result.add_issue(
  624. ValidationCategory.INTEGRITY,
  625. ValidationSeverity.ERROR,
  626. f"Error validating JSON file: {str(e)}",
  627. code="JSON_VALIDATION_ERROR"
  628. )
  629. def _validate_yaml_text(self, file_path: str, result: AssetValidationResult):
  630. """Validate YAML text file."""
  631. if not HAS_YAML:
  632. result.add_issue(
  633. ValidationCategory.FORMAT,
  634. ValidationSeverity.WARNING,
  635. "YAML validation skipped (PyYAML not available)",
  636. code="YAML_VALIDATOR_UNAVAILABLE"
  637. )
  638. return
  639. try:
  640. with open(file_path, 'r', encoding='utf-8') as f:
  641. content = f.read()
  642. try:
  643. data = yaml.safe_load(content)
  644. result.metadata.update({
  645. 'yaml_valid': True,
  646. 'yaml_type': type(data).__name__,
  647. 'character_count': len(content)
  648. })
  649. except yaml.YAMLError as e:
  650. result.add_issue(
  651. ValidationCategory.CONTENT,
  652. ValidationSeverity.ERROR,
  653. f"Invalid YAML syntax: {str(e)}",
  654. code="YAML_SYNTAX_ERROR"
  655. )
  656. except UnicodeDecodeError as e:
  657. result.add_issue(
  658. ValidationCategory.FORMAT,
  659. ValidationSeverity.ERROR,
  660. f"Text encoding error: {str(e)}",
  661. code="TEXT_ENCODING_ERROR"
  662. )
  663. except Exception as e:
  664. result.add_issue(
  665. ValidationCategory.INTEGRITY,
  666. ValidationSeverity.ERROR,
  667. f"Error validating YAML file: {str(e)}",
  668. code="YAML_VALIDATION_ERROR"
  669. )
  670. def _validate_plain_text(self, file_path: str, result: AssetValidationResult):
  671. """Validate plain text file."""
  672. try:
  673. encodings = ['utf-8', 'utf-16', 'latin-1', 'cp1252']
  674. content = None
  675. encoding_used = None
  676. for encoding in encodings:
  677. try:
  678. with open(file_path, 'r', encoding=encoding) as f:
  679. content = f.read()
  680. encoding_used = encoding
  681. break
  682. except UnicodeDecodeError:
  683. continue
  684. if content is not None:
  685. result.metadata.update({
  686. 'encoding': encoding_used,
  687. 'character_count': len(content),
  688. 'line_count': content.count('\n') + 1,
  689. 'word_count': len(content.split()),
  690. 'has_unicode': any(ord(char) > 127 for char in content)
  691. })
  692. else:
  693. result.add_issue(
  694. ValidationCategory.FORMAT,
  695. ValidationSeverity.ERROR,
  696. "Could not decode text file with any standard encoding",
  697. code="TEXT_ENCODING_UNKNOWN"
  698. )
  699. except Exception as e:
  700. result.add_issue(
  701. ValidationCategory.INTEGRITY,
  702. ValidationSeverity.ERROR,
  703. f"Error validating text file: {str(e)}",
  704. code="TEXT_VALIDATION_ERROR"
  705. )
  706. def _validate_json_config(self, file_path: str, result: AssetValidationResult):
  707. """Validate JSON configuration file."""
  708. # Start with basic JSON validation
  709. self._validate_json_text(file_path, result)
  710. if result.is_valid:
  711. # Additional config-specific checks could be added here
  712. # For example, schema validation, required fields, etc.
  713. pass
  714. def _validate_yaml_config(self, file_path: str, result: AssetValidationResult):
  715. """Validate YAML configuration file."""
  716. # Start with basic YAML validation
  717. self._validate_yaml_text(file_path, result)
  718. if result.is_valid:
  719. # Additional config-specific checks could be added here
  720. pass
  721. def _validate_pytorch_model(self, file_path: str, result: AssetValidationResult):
  722. """Validate PyTorch model file."""
  723. try:
  724. # Basic file signature check for pickle/torch files
  725. with open(file_path, 'rb') as f:
  726. header = f.read(8)
  727. # PyTorch files are typically pickle files
  728. if not header.startswith(b'\x80'): # Pickle protocol marker
  729. result.add_issue(
  730. ValidationCategory.FORMAT,
  731. ValidationSeverity.WARNING,
  732. "File may not be a valid PyTorch model (unexpected format)",
  733. code="PYTORCH_UNEXPECTED_FORMAT"
  734. )
  735. # Try to load with torch if available
  736. try:
  737. import torch
  738. # Don't actually load the model, just check if it's loadable
  739. # This is a placeholder - actual implementation would be more careful
  740. result.metadata['pytorch_available'] = True
  741. except ImportError:
  742. result.add_issue(
  743. ValidationCategory.FORMAT,
  744. ValidationSeverity.WARNING,
  745. "PyTorch not available for model validation",
  746. code="PYTORCH_UNAVAILABLE"
  747. )
  748. except Exception as e:
  749. result.add_issue(
  750. ValidationCategory.INTEGRITY,
  751. ValidationSeverity.ERROR,
  752. f"Error validating PyTorch model: {str(e)}",
  753. code="PYTORCH_VALIDATION_ERROR"
  754. )
  755. def _validate_python_script(self, file_path: str, result: AssetValidationResult):
  756. """Validate Python script file."""
  757. try:
  758. with open(file_path, 'r', encoding='utf-8') as f:
  759. content = f.read()
  760. # Check Python syntax
  761. try:
  762. compile(content, file_path, 'exec')
  763. result.metadata['python_syntax_valid'] = True
  764. except SyntaxError as e:
  765. result.add_issue(
  766. ValidationCategory.CONTENT,
  767. ValidationSeverity.ERROR,
  768. f"Python syntax error: {str(e)}",
  769. code="PYTHON_SYNTAX_ERROR",
  770. location=f"line {e.lineno}"
  771. )
  772. # Basic metrics
  773. lines = content.split('\n')
  774. result.metadata.update({
  775. 'line_count': len(lines),
  776. 'character_count': len(content),
  777. 'has_shebang': content.startswith('#!')
  778. })
  779. except UnicodeDecodeError as e:
  780. result.add_issue(
  781. ValidationCategory.FORMAT,
  782. ValidationSeverity.ERROR,
  783. f"Python script encoding error: {str(e)}",
  784. code="PYTHON_ENCODING_ERROR"
  785. )
  786. except Exception as e:
  787. result.add_issue(
  788. ValidationCategory.INTEGRITY,
  789. ValidationSeverity.ERROR,
  790. f"Error validating Python script: {str(e)}",
  791. code="PYTHON_VALIDATION_ERROR"
  792. )
  793. def _validate_generic_file(self, file_path: str, result: AssetValidationResult):
  794. """Generic validation for unknown file types."""
  795. try:
  796. # Basic file information
  797. path = Path(file_path)
  798. result.metadata.update({
  799. 'file_extension': path.suffix,
  800. 'mime_type': mimetypes.guess_type(str(path))[0]
  801. })
  802. # Check if file is readable
  803. with open(file_path, 'rb') as f:
  804. first_bytes = f.read(1024) # Read first 1KB
  805. result.metadata.update({
  806. 'is_binary': b'\x00' in first_bytes,
  807. 'first_bytes_length': len(first_bytes)
  808. })
  809. except Exception as e:
  810. result.add_issue(
  811. ValidationCategory.INTEGRITY,
  812. ValidationSeverity.ERROR,
  813. f"Error during generic validation: {str(e)}",
  814. code="GENERIC_VALIDATION_ERROR"
  815. )
  816. def _perform_security_checks(self, file_path: str, asset_type: str, result: AssetValidationResult):
  817. """Perform basic security checks on the asset."""
  818. if not self.enable_security_checks:
  819. return
  820. try:
  821. # Only scan text-based files for security issues
  822. if asset_type.startswith(('text/', 'config/', 'script/')):
  823. self._scan_text_security(file_path, result)
  824. # Check file permissions (if available)
  825. self._check_file_permissions(file_path, result)
  826. except Exception as e:
  827. result.add_issue(
  828. ValidationCategory.SECURITY,
  829. ValidationSeverity.WARNING,
  830. f"Security scan error: {str(e)}",
  831. code="SECURITY_SCAN_ERROR"
  832. )
  833. def _scan_text_security(self, file_path: str, result: AssetValidationResult):
  834. """Scan text files for security issues."""
  835. try:
  836. with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
  837. content = f.read(10240) # Read first 10KB for scanning
  838. for category, patterns in self._security_patterns.items():
  839. for pattern in patterns:
  840. matches = pattern.findall(content)
  841. if matches:
  842. result.add_issue(
  843. ValidationCategory.SECURITY,
  844. ValidationSeverity.WARNING,
  845. f"Potential {category.replace('_', ' ')} detected: {len(matches)} matches",
  846. code=f"SECURITY_{category.upper()}",
  847. suggestion="Review content for security implications"
  848. )
  849. except Exception:
  850. # Ignore errors in security scanning
  851. pass
  852. def _check_file_permissions(self, file_path: str, result: AssetValidationResult):
  853. """Check file permissions for security issues."""
  854. try:
  855. path = Path(file_path)
  856. stat = path.stat()
  857. # Check for world-writable files
  858. if stat.st_mode & 0o002:
  859. result.add_issue(
  860. ValidationCategory.SECURITY,
  861. ValidationSeverity.WARNING,
  862. "File is world-writable",
  863. code="SECURITY_WORLD_WRITABLE",
  864. suggestion="Consider restricting file permissions"
  865. )
  866. result.metadata['file_permissions'] = oct(stat.st_mode)[-3:]
  867. except Exception:
  868. # Ignore permission check errors
  869. pass
  870. def _analyze_performance_characteristics(self, file_path: str, asset_type: str, result: AssetValidationResult):
  871. """Analyze performance characteristics of the asset."""
  872. try:
  873. file_size = result.file_size
  874. # General size-based performance warnings
  875. if file_size > 50 * 1024 * 1024: # 50MB
  876. result.add_issue(
  877. ValidationCategory.PERFORMANCE,
  878. ValidationSeverity.WARNING,
  879. f"Large file size ({file_size / 1024 / 1024:.1f} MB) may impact loading performance",
  880. code="PERFORMANCE_LARGE_FILE",
  881. suggestion="Consider optimizing or compressing the asset"
  882. )
  883. # Type-specific performance analysis
  884. if asset_type.startswith('image/'):
  885. self._analyze_image_performance(file_path, result)
  886. elif asset_type.startswith('audio/'):
  887. self._analyze_audio_performance(file_path, result)
  888. except Exception:
  889. # Ignore performance analysis errors
  890. pass
  891. def _analyze_image_performance(self, file_path: str, result: AssetValidationResult):
  892. """Analyze image performance characteristics."""
  893. try:
  894. if 'width' in result.metadata and 'height' in result.metadata:
  895. width = result.metadata['width']
  896. height = result.metadata['height']
  897. pixel_count = width * height
  898. if pixel_count > 16 * 1024 * 1024: # 16 megapixels
  899. result.add_issue(
  900. ValidationCategory.PERFORMANCE,
  901. ValidationSeverity.WARNING,
  902. f"High resolution image ({width}x{height}) may impact performance",
  903. code="PERFORMANCE_HIGH_RESOLUTION",
  904. suggestion="Consider resizing for better performance"
  905. )
  906. except Exception:
  907. pass
  908. def _analyze_audio_performance(self, file_path: str, result: AssetValidationResult):
  909. """Analyze audio performance characteristics."""
  910. try:
  911. if 'sample_rate' in result.metadata and 'channels' in result.metadata:
  912. sample_rate = result.metadata['sample_rate']
  913. channels = result.metadata['channels']
  914. if sample_rate > 48000:
  915. result.add_issue(
  916. ValidationCategory.PERFORMANCE,
  917. ValidationSeverity.INFO,
  918. f"High sample rate ({sample_rate} Hz) may be overkill for voice assistant",
  919. code="PERFORMANCE_HIGH_SAMPLE_RATE",
  920. suggestion="Consider using 44.1kHz or 48kHz for better performance"
  921. )
  922. if channels > 2:
  923. result.add_issue(
  924. ValidationCategory.PERFORMANCE,
  925. ValidationSeverity.INFO,
  926. f"Multi-channel audio ({channels} channels) may not be needed",
  927. code="PERFORMANCE_MULTI_CHANNEL",
  928. suggestion="Consider using mono or stereo audio"
  929. )
  930. except Exception:
  931. pass
  932. class AssetIntegrityChecker:
  933. """
  934. Specialized integrity checker for assets.
  935. Provides deeper integrity checking including checksum verification,
  936. format consistency, and corruption detection.
  937. """
  938. def __init__(self):
  939. """Initialize the integrity checker."""
  940. self._checksum_cache: Dict[str, Tuple[str, float]] = {} # path -> (checksum, mtime)
  941. pprint("AssetIntegrityChecker initialized")
  942. def check_integrity(self, file_path: str, expected_checksum: str = None) -> bool:
  943. """
  944. Check file integrity using checksums.
  945. Args:
  946. file_path: Path to the file to check
  947. expected_checksum: Expected SHA256 checksum (optional)
  948. Returns:
  949. True if integrity check passes, False otherwise
  950. """
  951. try:
  952. current_checksum = self._get_checksum(file_path)
  953. if expected_checksum:
  954. return current_checksum == expected_checksum
  955. else:
  956. # If no expected checksum, just verify we can calculate one
  957. return current_checksum is not None
  958. except Exception:
  959. return False
  960. def _get_checksum(self, file_path: str) -> str:
  961. """Get cached or calculate checksum for a file."""
  962. try:
  963. path = Path(file_path)
  964. mtime = path.stat().st_mtime
  965. # Check cache
  966. if file_path in self._checksum_cache:
  967. cached_checksum, cached_mtime = self._checksum_cache[file_path]
  968. if cached_mtime == mtime:
  969. return cached_checksum
  970. # Calculate new checksum
  971. hash_sha256 = hashlib.sha256()
  972. with open(file_path, 'rb') as f:
  973. for chunk in iter(lambda: f.read(4096), b""):
  974. hash_sha256.update(chunk)
  975. checksum = hash_sha256.hexdigest()
  976. # Update cache
  977. self._checksum_cache[file_path] = (checksum, mtime)
  978. return checksum
  979. except Exception:
  980. return None
  981. def clear_cache(self):
  982. """Clear the checksum cache."""
  983. self._checksum_cache.clear()
  984. # Convenience functions
  985. def quick_validate(file_path: str, asset_type: str = None, strict: bool = False) -> AssetValidationResult:
  986. """
  987. Quick validation of an asset file.
  988. Args:
  989. file_path: Path to the asset file
  990. asset_type: Asset type (will be detected if None)
  991. strict: Whether to use strict mode
  992. Returns:
  993. AssetValidationResult
  994. """
  995. validator = AssetValidator(strict_mode=strict)
  996. return validator.validate_asset(file_path, asset_type)
  997. def batch_validate(file_paths: List[str], asset_types: List[str] = None) -> List[AssetValidationResult]:
  998. """
  999. Validate multiple assets in batch.
  1000. Args:
  1001. file_paths: List of file paths to validate
  1002. asset_types: List of asset types (optional)
  1003. Returns:
  1004. List of AssetValidationResult objects
  1005. """
  1006. validator = AssetValidator()
  1007. results = []
  1008. for i, file_path in enumerate(file_paths):
  1009. asset_type = asset_types[i] if asset_types and i < len(asset_types) else None
  1010. result = validator.validate_asset(file_path, asset_type)
  1011. results.append(result)
  1012. return results
  1013. # Module exports
  1014. __all__ = [
  1015. 'AssetValidator',
  1016. 'AssetIntegrityChecker',
  1017. 'AssetValidationResult',
  1018. 'ValidationIssue',
  1019. 'ValidationSeverity',
  1020. 'ValidationCategory',
  1021. 'quick_validate',
  1022. 'batch_validate',
  1023. 'pprint'
  1024. ]