using System; using System.Collections.Generic; using System.Linq; using System.Text; using GeViSetEditor.Core.Models; namespace GeViSetEditor.Core.Parsers { /// /// Enhanced parser that extracts full configuration structure /// while preserving original binary data for round-trip conversion /// public class EnhancedSetFileParser { private byte[] _originalData; private int _position; public EnhancedSetFile Parse(byte[] data) { _originalData = data; _position = 0; var setFile = new EnhancedSetFile { OriginalData = data, FileSize = data.Length }; Console.WriteLine($"\n=== Enhanced Configuration Parser ==="); Console.WriteLine($"File size: {data.Length:N0} bytes\n"); // Read header if (_position < data.Length && data[_position] == 0x00) _position++; string header = ReadPascalString(); setFile.Header = header ?? "GeViSoft Parameters"; Console.WriteLine($"Header: '{setFile.Header}'"); // Extract configuration items (key-value pairs) ExtractConfigItems(setFile); // Extract action mappings ExtractActionMappings(setFile); // Extract section names ExtractSectionNames(setFile); Console.WriteLine($"\n=== Parse Summary ==="); Console.WriteLine($"Configuration items: {setFile.ConfigItems.Count}"); Console.WriteLine($"Action mappings: {setFile.ActionMappings.Count}"); Console.WriteLine($"Section names: {setFile.SectionNames.Count}"); return setFile; } private void ExtractConfigItems(EnhancedSetFile setFile) { Console.WriteLine("\nExtracting configuration items..."); // Look for key-value patterns // Common pattern: Pascal string (key) followed by typed value for (int i = 0; i < _originalData.Length - 10; i++) { var item = TryReadConfigItem(i); if (item != null && !string.IsNullOrWhiteSpace(item.Key)) { setFile.ConfigItems.Add(item); } } Console.WriteLine($"Found {setFile.ConfigItems.Count} config items"); } private ConfigItemEntry TryReadConfigItem(int offset) { try { // Skip if this looks like part of an action mapping if (IsNearRulesMarker(offset, 100)) return null; // Skip if in the first 1000 bytes (header area) if (offset < 1000) return null; // Try to read Pascal string as key int pos = offset; // Look for string marker (0x07) followed by reasonable length if (pos >= _originalData.Length || _originalData[pos] != 0x07) return null; pos++; if (pos >= _originalData.Length) return null; byte keyLen = _originalData[pos]; if (keyLen < 3 || keyLen > 50) // More restrictive length return null; pos++; if (pos + keyLen > _originalData.Length) return null; string key = Encoding.UTF8.GetString(_originalData, pos, keyLen); // Check if key looks valid (must be clean identifier) if (!IsValidConfigKey(key)) return null; // Key must not contain common section names (avoid duplicates) string keyLower = key.ToLower(); if (keyLower.Contains("rules") || keyLower.Contains("action") || keyLower.Contains("gsc") || keyLower.Contains("gng")) return null; pos += keyLen; // Must be followed immediately by typed value marker if (pos >= _originalData.Length) return null; byte typeMarker = _originalData[pos]; if (typeMarker != 0x01 && typeMarker != 0x04 && typeMarker != 0x07) return null; // Try to read value var (value, valueType, bytesRead) = TryReadTypedValue(pos); if (value == null || bytesRead == 0) return null; return new ConfigItemEntry { FileOffset = offset, Key = key, Value = value, ValueType = valueType }; } catch { return null; } } private (object value, string type, int bytesRead) TryReadTypedValue(int pos) { if (pos >= _originalData.Length) return (null, null, 0); byte typeMarker = _originalData[pos]; try { switch (typeMarker) { case 0x01: // Boolean if (pos + 2 <= _originalData.Length) { bool value = _originalData[pos + 1] != 0; return (value, "boolean", 2); } break; case 0x04: // Integer (4 bytes) if (pos + 5 <= _originalData.Length) { int value = BitConverter.ToInt32(_originalData, pos + 1); return (value, "integer", 5); } break; case 0x07: // String (Pascal string) pos++; if (pos >= _originalData.Length) break; byte strLen = _originalData[pos]; if (strLen > 0 && strLen < 200 && pos + 1 + strLen <= _originalData.Length) { string value = Encoding.UTF8.GetString(_originalData, pos + 1, strLen); if (IsValidStringValue(value)) { return (value, "string", 2 + strLen); } } break; } } catch { // Ignore parse errors } return (null, null, 0); } private bool IsValidConfigKey(string key) { if (string.IsNullOrWhiteSpace(key)) return false; // Must not be too long if (key.Length > 40) return false; // No control characters except spaces if (key.Any(c => char.IsControl(c))) return false; // Should contain mostly alphanumeric or underscores int validChars = key.Count(c => char.IsLetterOrDigit(c) || c == '_' || c == '-'); // At least 90% valid chars (more strict) if (validChars < key.Length * 0.9) return false; // Must start with a letter or underscore if (!char.IsLetter(key[0]) && key[0] != '_') return false; // Common patterns that indicate it's not a config key if (key.Contains(" ") || key.Contains("\t")) return false; return true; } private bool IsValidStringValue(string value) { if (string.IsNullOrEmpty(value)) return true; // Empty strings are valid // Check for excessive control characters int controlChars = value.Count(c => char.IsControl(c) && c != '\r' && c != '\n' && c != '\t'); return controlChars < value.Length * 0.3; // Less than 30% control chars } private bool IsNearRulesMarker(int offset, int distance) { // Check if there's a "Rules" marker nearby byte[] rulesPattern = new byte[] { 0x05, 0x52, 0x75, 0x6C, 0x65, 0x73 }; int start = Math.Max(0, offset - distance); int end = Math.Min(_originalData.Length - rulesPattern.Length, offset + distance); for (int i = start; i < end; i++) { if (MatchesPattern(_originalData, i, rulesPattern)) return true; } return false; } private void ExtractActionMappings(EnhancedSetFile setFile) { Console.WriteLine("\nExtracting action mappings..."); // Search for all "Rules" patterns (05 52 75 6C 65 73) byte[] rulesPattern = new byte[] { 0x05, 0x52, 0x75, 0x6C, 0x65, 0x73 }; for (int i = 0; i < _originalData.Length - rulesPattern.Length - 100; i++) { if (MatchesPattern(_originalData, i, rulesPattern)) { var mapping = TryExtractActionMapping(i); if (mapping != null) { setFile.ActionMappings.Add(mapping); } } } Console.WriteLine($"Found {setFile.ActionMappings.Count} action mappings"); } private ActionMappingEntry TryExtractActionMapping(int rulesOffset) { try { var entry = new ActionMappingEntry { FileOffset = rulesOffset, RulesMarkerOffset = rulesOffset }; // Position after "Rules" marker int pos = rulesOffset + 6; // Skip "05 Rules" if (pos + 10 > _originalData.Length) return null; // Skip metadata bytes (usually 0x00-0x04) while (pos < _originalData.Length && _originalData[pos] <= 0x04) { pos++; } int actionDataStart = pos; // Extract action strings using pattern 07 01 40 var actions = new List(); int consecutiveFailures = 0; int maxAttempts = 200; while (maxAttempts-- > 0 && pos < _originalData.Length - 10) { var action = TryReadActionStringAt(pos); if (action != null) { actions.Add(action); pos += 5 + Encoding.UTF8.GetByteCount(action); consecutiveFailures = 0; } else { pos++; consecutiveFailures++; if (consecutiveFailures > 100) break; } } if (actions.Count > 0) { entry.Actions = actions; entry.ActionDataStartOffset = actionDataStart; entry.ActionDataEndOffset = pos; // Store original bytes int length = pos - rulesOffset; entry.OriginalBytes = new byte[length]; Array.Copy(_originalData, rulesOffset, entry.OriginalBytes, 0, length); return entry; } } catch (Exception ex) { Console.WriteLine($"Error at offset {rulesOffset}: {ex.Message}"); } return null; } private string TryReadActionStringAt(int pos) { // Pattern: 07 01 40 if (pos + 5 > _originalData.Length) return null; if (_originalData[pos] != 0x07 || _originalData[pos + 1] != 0x01 || _originalData[pos + 2] != 0x40) return null; ushort length = BitConverter.ToUInt16(_originalData, pos + 3); if (length == 0 || length > 500 || pos + 5 + length > _originalData.Length) return null; try { string result = Encoding.UTF8.GetString(_originalData, pos + 5, length); // Validate it looks like an action string if (result.Length > 3 && result.All(c => c >= 32 || c == '\t' || c == '\n' || c == '\r')) { return result; } } catch { // Ignore decode errors } return null; } private void ExtractSectionNames(EnhancedSetFile setFile) { Console.WriteLine("\nExtracting section names..."); // Look for Pascal strings that appear to be section names // Characteristics: reasonable length (5-50 chars), mostly alphanumeric HashSet foundNames = new HashSet(); for (int i = 0; i < _originalData.Length - 10; i++) { if (_originalData[i] == 0x07) // String marker { i++; if (i >= _originalData.Length) break; byte len = _originalData[i]; if (len >= 5 && len <= 50 && i + 1 + len <= _originalData.Length) { try { string name = Encoding.UTF8.GetString(_originalData, i + 1, len); // Check if it looks like a section name if (IsLikelySectionName(name) && !foundNames.Contains(name)) { foundNames.Add(name); setFile.SectionNames.Add(new SectionNameEntry { FileOffset = i - 1, Name = name }); } } catch { // Ignore decode errors } } } } Console.WriteLine($"Found {setFile.SectionNames.Count} unique section names"); } private bool IsLikelySectionName(string name) { if (string.IsNullOrWhiteSpace(name)) return false; // Should be mostly letters, numbers, underscores // Common section names: "Alarms", "Cameras", "GeViIO", "Description", "Name", "IpHost" int validChars = name.Count(c => char.IsLetterOrDigit(c) || c == '_' || c == ' '); // At least 80% valid characters return validChars >= name.Length * 0.8; } private string ReadPascalString() { if (_position >= _originalData.Length) return null; byte length = _originalData[_position++]; if (length == 0 || _position + length > _originalData.Length) return null; string result = Encoding.UTF8.GetString(_originalData, _position, length); _position += length; return result; } private bool MatchesPattern(byte[] data, int offset, byte[] pattern) { if (offset + pattern.Length > data.Length) return false; for (int i = 0; i < pattern.Length; i++) { if (data[offset + i] != pattern[i]) return false; } return true; } } }