using System; using System.Collections.Generic; using System.Text; using GeViSetEditor.Core.Models; namespace GeViSetEditor.Core.Parsers { /// /// Complete binary parser that understands the entire .set file structure /// Parses ALL sections, items, and rules for full JSON serialization /// public class CompleteBinaryParser { private byte[] _data; private int _position; private Dictionary _debugLog = new(); public SetFileComplete Parse(byte[] data) { _data = data; _position = 0; var setFile = new SetFileComplete { FileSize = data.Length, RawData = data // Keep original for verification }; Console.WriteLine($"=== Complete Binary Parser ==="); Console.WriteLine($"File size: {data.Length:N0} bytes\n"); try { // Parse header ParseHeader(setFile); // Parse all sections ParseAllSections(setFile); Console.WriteLine($"\n=== Parsing Complete ==="); Console.WriteLine($"Sections parsed: {setFile.Sections.Count}"); Console.WriteLine($"Total items: {setFile.Sections.Sum(s => s.Items.Count)}"); Console.WriteLine($"Total rules: {setFile.Sections.Sum(s => s.Rules.Count)}"); return setFile; } catch (Exception ex) { Console.WriteLine($"\nERROR at position {_position} (0x{_position:X}): {ex.Message}"); throw; } } private void ParseHeader(SetFileComplete setFile) { Console.WriteLine("Parsing header..."); // Optional null byte if (_position < _data.Length && _data[_position] == 0x00) { setFile.HeaderNullByte = true; _position++; } // Header is length-prefixed string (NOT Pascal string with 0x07) if (_position + 2 > _data.Length) throw new Exception("File too short for header"); byte headerLen = _data[_position++]; if (headerLen == 0 || headerLen > 50 || _position + headerLen > _data.Length) throw new Exception($"Invalid header length: {headerLen}"); setFile.Header = Encoding.UTF8.GetString(_data, _position, headerLen); _position += headerLen; Console.WriteLine($" Header: '{setFile.Header}'"); Console.WriteLine($" Position after header: {_position} (0x{_position:X})\n"); } private void ParseAllSections(SetFileComplete setFile) { Console.WriteLine("Parsing sections...\n"); int sectionCount = 0; int maxIterations = 10000; // Safety limit while (_position < _data.Length - 10 && maxIterations-- > 0) { int sectionStart = _position; var section = TryParseSection(); if (section != null) { sectionCount++; setFile.Sections.Add(section); if (sectionCount <= 20 || section.Items.Count > 0 || section.Rules.Count > 0) { Console.WriteLine($"Section #{sectionCount}: '{section.Name}' " + $"(offset {sectionStart}, {section.Items.Count} items, {section.Rules.Count} rules)"); } } else { // Skip unknown byte _position++; } } Console.WriteLine($"\nTotal sections parsed: {sectionCount}"); } private SectionComplete TryParseSection() { int startPos = _position; try { // Try to read section name (Pascal string: 0x07 ) string sectionName = TryReadPascalString(); if (string.IsNullOrEmpty(sectionName) || sectionName.Length > 100) { _position = startPos; return null; } var section = new SectionComplete { Name = sectionName, FileOffset = startPos }; // Parse section contents ParseSectionContents(section); section.FileEndOffset = _position; return section; } catch { _position = startPos; return null; } } private void ParseSectionContents(SectionComplete section) { int maxIterations = 1000; bool foundRules = false; while (_position < _data.Length - 10 && maxIterations-- > 0) { int itemStart = _position; // Check for "Rules" marker (length-prefixed, no 0x07) if (CheckForRulesMarker()) { foundRules = true; ParseRulesSubsection(section); break; // Rules typically end a section } // Try to parse a config item var item = TryParseConfigItem(); if (item != null) { section.Items.Add(item); continue; } // Check if we've hit next section if (IsLikelyNextSection()) { break; } // Move forward _position++; } } private bool CheckForRulesMarker() { // Pattern: 05 52 75 6C 65 73 = "Rules" (length 5 + data) if (_position + 6 > _data.Length) return false; return _data[_position] == 0x05 && _data[_position + 1] == 0x52 && // 'R' _data[_position + 2] == 0x75 && // 'u' _data[_position + 3] == 0x6C && // 'l' _data[_position + 4] == 0x65 && // 'e' _data[_position + 5] == 0x73; // 's' } private void ParseRulesSubsection(SectionComplete section) { // Skip "Rules" marker _position += 6; // Skip metadata bytes (counts, flags, etc.) while (_position < _data.Length && _data[_position] <= 0x04) { _position++; } // Parse action rules int ruleCount = 0; int maxRules = 100; int consecutiveFails = 0; while (maxRules-- > 0 && _position < _data.Length - 20) { var rule = TryParseActionRule(); if (rule != null) { rule.RuleId = ruleCount++; section.Rules.Add(rule); consecutiveFails = 0; } else { _position++; consecutiveFails++; if (consecutiveFails > 100) break; } } } private ActionRuleComplete TryParseActionRule() { int startPos = _position; try { var rule = new ActionRuleComplete { FileOffset = startPos }; // Try to read action string (07 01 40 ) string actionString = TryReadActionString(); if (!string.IsNullOrEmpty(actionString)) { rule.Actions.Add(actionString); return rule; } _position = startPos; return null; } catch { _position = startPos; return null; } } private ConfigItemComplete TryParseConfigItem() { int startPos = _position; try { // Read key (Pascal string) string key = TryReadPascalString(); if (string.IsNullOrEmpty(key) || key.Length > 200) { _position = startPos; return null; } // Read value (typed) var (value, type) = TryReadTypedValue(); if (value == null) { _position = startPos; return null; } return new ConfigItemComplete { Key = key, Value = value, ValueType = type, FileOffset = startPos }; } catch { _position = startPos; return null; } } private string TryReadPascalString() { if (_position + 2 > _data.Length || _data[_position] != 0x07) return null; byte length = _data[_position + 1]; if (length == 0 || length > 200 || _position + 2 + length > _data.Length) return null; string result = Encoding.UTF8.GetString(_data, _position + 2, length); _position += 2 + length; return result; } private string TryReadActionString() { // Pattern: 07 01 40 if (_position + 5 > _data.Length) return null; if (_data[_position] != 0x07 || _data[_position + 1] != 0x01 || _data[_position + 2] != 0x40) return null; int length = BitConverter.ToUInt16(_data, _position + 3); if (length == 0 || length > 500 || _position + 5 + length > _data.Length) return null; string result = Encoding.UTF8.GetString(_data, _position + 5, length); _position += 5 + length; // Validate it looks like an action if (string.IsNullOrWhiteSpace(result) || result.Length < 3) return null; return result; } private (object value, string type) TryReadTypedValue() { if (_position >= _data.Length) return (null, null); byte typeMarker = _data[_position]; switch (typeMarker) { case 0x01: // Boolean if (_position + 2 <= _data.Length) { _position++; bool value = _data[_position++] != 0; return (value, "boolean"); } break; case 0x04: // Integer (4 bytes, little-endian) if (_position + 5 <= _data.Length) { _position++; int value = BitConverter.ToInt32(_data, _position); _position += 4; return (value, "integer"); } break; case 0x07: // String (Pascal string) string value_str = TryReadPascalString(); if (value_str != null) { return (value_str, "string"); } break; } return (null, null); } private bool IsLikelyNextSection() { // Check if next bytes look like a new section (Pascal string) if (_position + 3 >= _data.Length) return true; if (_data[_position] != 0x07) return false; byte length = _data[_position + 1]; if (length < 3 || length > 50) return false; // Check if it's printable ASCII (likely a section name) for (int i = 0; i < Math.Min(5, (int)length); i++) { if (_position + 2 + i >= _data.Length) return false; byte b = _data[_position + 2 + i]; if (b < 32 || b > 126) return false; } return true; } } }