using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Linq; using GeViSetEditor.Core.Models; namespace GeViSetEditor.Core.Parsers { /// /// Parser for GeViSet .set configuration files (binary format) /// Format: Proprietary binary with sections, rules, and action mappings /// public class SetFileParser { private byte[] _data; private int _position; public GeViSetConfiguration Parse(string filePath) { _data = File.ReadAllBytes(filePath); _position = 0; var config = new GeViSetConfiguration(); // Read header if (_data[_position] == 0x00) _position++; // Skip initial marker config.Header = ReadPascalString() ?? "GeViSoft Parameters"; Console.WriteLine($"Parsing {filePath} ({_data.Length:N0} bytes)"); Console.WriteLine($"Header: {config.Header}\n"); // Parse all sections until end of file int sectionCount = 0; while (_position < _data.Length - 20) { var section = TryParseSection(); if (section != null) { config.Sections.Add(section); sectionCount++; Console.WriteLine($"Section {sectionCount}: {section.Name} ({section.Items.Count} items, {section.Rules.Count} rules) - pos: {_position}"); } else { _position++; // Skip unknown byte } } Console.WriteLine($"\nTotal: {sectionCount} sections parsed"); // Second pass: Find all "Rules" markers and extract action mappings directly Console.WriteLine($"\nSearching for Rules sections..."); ExtractRulesDirectly(config); return config; } private void ExtractRulesDirectly(GeViSetConfiguration config) { // Search for pattern: 05 52 75 6C 65 73 = "05 Rules" byte[] rulesPattern = new byte[] { 0x05, 0x52, 0x75, 0x6C, 0x65, 0x73 }; for (int i = 0; i < _data.Length - rulesPattern.Length; i++) { bool match = true; for (int j = 0; j < rulesPattern.Length; j++) { if (_data[i + j] != rulesPattern[j]) { match = false; break; } } if (match) { Console.WriteLine($" Found Rules pattern at offset {i} (0x{i:X})"); _position = i + rulesPattern.Length; // Create or find a section for these rules var rulesSection = config.Sections.FirstOrDefault(s => s.Name == "ActionMappings"); if (rulesSection == null) { rulesSection = new Section { Name = "ActionMappings" }; config.Sections.Add(rulesSection); } // Try to parse rules from this location ParseRulesSection(rulesSection); Console.WriteLine($" Extracted {rulesSection.Rules.Count} rules"); } } } private Section TryParseSection() { int startPos = _position; // Try to read section name string sectionName = ReadPascalString(); if (string.IsNullOrEmpty(sectionName) || sectionName.Length > 50) { _position = startPos; return null; } var section = new Section { Name = sectionName }; // Parse section contents int maxIterations = 1000; // Safety limit while (_position < _data.Length - 10 && maxIterations-- > 0) { // Check for "Rules" subsection (can be Pascal string OR length-prefixed) int savePos = _position; // Try Pascal string first (0x07 marker) string possibleRules = ReadPascalString(); if (possibleRules == "Rules") { Console.WriteLine($" Found Rules subsection (Pascal) at offset {savePos} in section '{sectionName}'"); ParseRulesSection(section); break; // Rules typically end a section } // Rewind and try length-prefixed format (no 0x07 marker) _position = savePos; possibleRules = ReadLengthPrefixedString(); if (possibleRules == "Rules") { Console.WriteLine($" Found Rules subsection (LenPrefix) at offset {savePos} in section '{sectionName}'"); ParseRulesSection(section); break; // Rules typically end a section } // Neither worked, rewind _position = savePos; // Try to read a config item var item = TryReadConfigItem(); if (item != null) { section.Items.Add(item); } else { // Check if we've hit the next section or end if (IsLikelySectionStart()) break; _position++; } } return section; } private void ParseRulesSection(Section section) { int startRuleCount = section.Rules.Count; int startPos = _position; // Skip count/metadata bytes while (_position < _data.Length && _data[_position] <= 0x04) { _position++; } Console.WriteLine($" ParseRulesSection: start={startPos}, after skip={_position}, bytes: {BitConverter.ToString(_data, _position, Math.Min(20, _data.Length - _position))}"); // Parse rules - keep trying until we stop finding action strings int maxRules = 100; int attempts = 0; int consecutiveFails = 0; while (maxRules-- > 0 && _position < _data.Length - 20) { int beforePos = _position; var rule = TryParseRule(); attempts++; if (rule != null) { section.Rules.Add(rule); Console.WriteLine($" Found rule at pos {beforePos}, now at {_position}"); consecutiveFails = 0; } else { _position++; consecutiveFails++; // Stop if we've had too many consecutive failures if (consecutiveFails > 100) { Console.WriteLine($" Too many consecutive failures at {_position}, stopping"); break; } } if (attempts > 200) break; // Safety limit } Console.WriteLine($" ParseRulesSection complete: extracted {section.Rules.Count - startRuleCount} rules"); } private ActionRule TryParseRule() { int startPos = _position; // Look for rule marker: 01 if (_position + 2 >= _data.Length || _data[_position] != 0x01) { _position = startPos; return null; } var rule = new ActionRule { RuleId = _data[_position + 1] }; _position += 2; // Skip size bytes (05 00 00 00 or 0a 00 00 00 patterns) if (_position + 4 < _data.Length && (_data[_position] == 0x05 || _data[_position] == 0x0a)) { _position += 4; } // Read trigger properties (fields starting with .) for (int i = 0; i < 30; i++) // Max 30 properties { if (_position >= _data.Length) break; // Check for property field (01 .) if (_data[_position] == 0x01 && _position + 2 < _data.Length) { int len = _data[_position + 1]; if (len > 0 && len < 50 && _position + 2 + len < _data.Length) { string propName = Encoding.UTF8.GetString(_data, _position + 2, len); _position += 2 + len; // Read boolean value if (_position < _data.Length) { bool value = _data[_position++] != 0; if (propName.StartsWith(".")) { rule.TriggerProperties[propName] = value; } } } else { break; } } else { break; } } // Read main action string (07 01 40 ) string mainAction = ReadActionString(); if (!string.IsNullOrEmpty(mainAction)) { rule.MainAction = mainAction; } // Skip metadata (04 02 40... patterns) SkipMetadata(); // Look for nested Rules section (action variations) int checkPos = _position; string nestedRules = ReadPascalString(); if (nestedRules == "Rules") { ParseActionVariations(rule); } else { // Try length-prefixed format _position = checkPos; nestedRules = ReadLengthPrefixedString(); if (nestedRules == "Rules") { ParseActionVariations(rule); } else { _position = checkPos; // Rewind if not "Rules" } } // Only return rule if it has meaningful data if (!string.IsNullOrEmpty(rule.MainAction) || rule.TriggerProperties.Count > 0) { return rule; } _position = startPos; return null; } private void ParseActionVariations(ActionRule rule) { // Skip count bytes while (_position < _data.Length && _data[_position] <= 0x04) { _position++; } // Parse each variation int maxVariations = 30; while (maxVariations-- > 0 && _position < _data.Length - 20) { var variation = TryParseActionVariation(); if (variation != null) { rule.ActionVariations.Add(variation); } else { if (IsLikelySectionStart()) break; // Look ahead for next variation bool foundNext = false; for (int i = 1; i < 20 && _position + i < _data.Length; i++) { if (_data[_position + i] == 0x01 && _position + i + 1 < _data.Length) { _position += i; foundNext = true; break; } } if (!foundNext) break; } } } private ActionVariation TryParseActionVariation() { int startPos = _position; // Look for variation marker: 01 if (_position + 2 >= _data.Length || _data[_position] != 0x01) { _position = startPos; return null; } var variation = new ActionVariation { VariationId = _data[_position + 1] }; _position += 2; // Skip size (05 00 00 00) if (_position + 4 < _data.Length && _data[_position] == 0x05) { _position += 4; } // Read action string variation.ActionString = ReadActionString() ?? ""; // Skip metadata SkipMetadata(); // Read action type (GscAction, GCoreAction, etc.) string actionType = ReadPascalString(); if (actionType != null && actionType.Contains("Action")) { variation.ActionType = actionType; // Read full command (another action string) variation.FullCommand = ReadActionString() ?? ""; // Read server type variation.ServerType = ReadPascalString() ?? ""; // Try to read server name int savePos = _position; string serverName = ReadPascalString(); if (!string.IsNullOrEmpty(serverName) && serverName.Length < 100 && !serverName.StartsWith(".")) { variation.ServerName = serverName; } else { _position = savePos; // Rewind if not a valid server name } } // Only return if we got meaningful data if (!string.IsNullOrEmpty(variation.ActionString)) { return variation; } _position = startPos; return null; } private ConfigItem TryReadConfigItem() { int startPos = _position; string name = ReadPascalString(); if (string.IsNullOrEmpty(name) || name.Length > 100) { _position = startPos; return null; } // Try to read value object value = ReadValue(); if (value == null) { _position = startPos; return null; } return new ConfigItem { Name = name, Value = value, Type = value switch { bool => ConfigValueType.Boolean, int => ConfigValueType.Integer, string => ConfigValueType.String, _ => ConfigValueType.Binary } }; } // Helper methods for reading binary data private string ReadPascalString() { if (_position + 2 > _data.Length || _data[_position] != 0x07) return null; byte length = _data[_position + 1]; if (_position + 2 + length > _data.Length || length == 0) return null; string result = Encoding.UTF8.GetString(_data, _position + 2, length); _position += 2 + length; return result; } private string ReadLengthPrefixedString() { // Simpler format: just (no 0x07 marker) if (_position + 1 > _data.Length) return null; byte length = _data[_position]; if (length == 0 || length > 50 || _position + 1 + length > _data.Length) return null; string result = Encoding.UTF8.GetString(_data, _position + 1, length); _position += 1 + length; return result; } private string ReadActionString() { // Action strings: 07 01 40 if (_position + 5 > _data.Length || _data[_position] != 0x07 || _data[_position + 1] != 0x01 || _data[_position + 2] != 0x40) return null; int length = BitConverter.ToUInt16(_data, _position + 3); if (_position + 5 + length > _data.Length || length > 500) return null; string result = Encoding.UTF8.GetString(_data, _position + 5, length); _position += 5 + length; return result; } private object ReadValue() { if (_position >= _data.Length) return null; byte type = _data[_position]; switch (type) { case 0x01: // Boolean if (_position + 2 <= _data.Length) { _position++; return _data[_position++] != 0; } break; case 0x04: // Integer if (_position + 5 <= _data.Length) { _position++; int value = BitConverter.ToInt32(_data, _position); _position += 4; return value; } break; case 0x07: // String return ReadPascalString(); } return null; } private void SkipMetadata() { // Skip common metadata patterns (04 02 40... and 04 0a...) while (_position + 5 < _data.Length && _data[_position] == 0x04) { _position += 5; } } private string PeekString(int length) { if (_position + length > _data.Length) return ""; return Encoding.UTF8.GetString(_data, _position, length); } private bool IsLikelySectionStart() { if (_position + 10 >= _data.Length) return true; // Check for multiple nulls (section boundary) int nullCount = 0; for (int i = 0; i < Math.Min(10, _data.Length - _position); i++) { if (_data[_position + i] == 0x00) nullCount++; } return nullCount >= 4; } } }