using System; using System.IO; using System.Text; using System.Linq; namespace V2GDecoderNet { class Program { private const int BUFFER_SIZE = 4096; // Network protocol patterns and definitions private const ushort ETH_TYPE_IPV6 = 0x86DD; // Ethernet Type: IPv6 private const byte IPV6_NEXT_HEADER_TCP = 0x06; // IPv6 Next Header: TCP private const ushort TCP_V2G_PORT = 15118; // V2G communication port // V2G Transfer Protocol patterns and definitions private const byte V2G_PROTOCOL_VERSION = 0x01; // Protocol Version private const byte V2G_INV_PROTOCOL_VERSION = 0xFE; // Inverse Protocol Version private const ushort V2G_PAYLOAD_ISO_DIN_SAP = 0x8001; // ISO 15118-2/DIN/SAP payload type private const ushort V2G_PAYLOAD_ISO2 = 0x8002; // ISO 15118-20 payload type private const ushort EXI_START_PATTERN = 0x8098; // EXI document start pattern static int Main(string[] args) { bool xmlMode = false; bool encodeMode = false; string filename = null; if (args.Length == 1) { filename = args[0]; // 자동으로 확장자를 감지하여 모드 결정 string extension = Path.GetExtension(filename).ToLower(); if (extension == ".xml") { encodeMode = true; Console.WriteLine($"Auto-detected XML file: encoding to EXI"); } else { Console.WriteLine($"Auto-detected binary file: analyzing structure"); } } else if (args.Length == 2 && args[0] == "-decode") { xmlMode = true; filename = args[1]; } else if (args.Length == 2 && args[0] == "-encode") { encodeMode = true; filename = args[1]; } else if (args.Length == 1 && args[0] == "-decode") { // stdin에서 EXI 읽어서 XML로 변환 return HandleStdinDecode(); } else if (args.Length == 1 && args[0] == "-encode") { // stdin에서 XML 읽어서 EXI로 변환 return HandleStdinEncode(); } else { Console.Error.WriteLine("Usage: V2GDecoderNet [-decode|-encode] input_file"); Console.Error.WriteLine(" V2GDecoderNet filename (auto-detect by extension)"); Console.Error.WriteLine(" V2GDecoderNet -encode (read XML from stdin)"); Console.Error.WriteLine(" V2GDecoderNet -decode (read hex string from stdin)"); Console.Error.WriteLine("Enhanced EXI viewer with XML conversion capabilities"); Console.Error.WriteLine(" filename Auto-detect: .xml files are encoded, others are decoded/analyzed"); Console.Error.WriteLine(" -decode Convert EXI to Wireshark-style XML format"); Console.Error.WriteLine(" -decode Read hex string from stdin (echo hex | V2GDecoderNet -decode)"); Console.Error.WriteLine(" -encode Convert XML to EXI format"); Console.Error.WriteLine(" -encode Read XML from stdin (type file.xml | V2GDecoderNet -encode)"); Console.Error.WriteLine(" (default) Analyze EXI with detailed output"); Console.Error.WriteLine(""); Console.Error.WriteLine("Contact: tindevil82@gmail.com"); return -1; } if (!File.Exists(filename)) { Console.Error.WriteLine($"Error reading file: {filename}"); return -1; } try { if (encodeMode) { return HandleEncodeMode(filename); } else { return HandleDecodeOrAnalyzeMode(filename, xmlMode); } } catch (Exception ex) { Console.Error.WriteLine($"Error processing file: {ex.Message}"); return -1; } } private static int HandleEncodeMode(string filename) { try { // Read XML file string xmlContent = File.ReadAllText(filename, Encoding.UTF8); // Parse and encode XML to EXI var exiData = V2GMessageProcessor.EncodeXmlToExi(xmlContent); if (exiData == null || exiData.Length == 0) { Console.Error.WriteLine("Error encoding XML to EXI"); return -1; } // Check if output is redirected bool isRedirected = Console.IsOutputRedirected; if (isRedirected) { // Binary output for redirection (file output) using (var stdout = Console.OpenStandardOutput()) { stdout.Write(exiData, 0, exiData.Length); stdout.Flush(); } } else { // Hex string output for console display Console.Write(BitConverter.ToString(exiData).Replace("-", "")); } return 0; } catch (Exception ex) { Console.Error.WriteLine($"Error encoding to EXI: {ex.Message}"); return -1; } } private static int HandleDecodeOrAnalyzeMode(string filename, bool xmlMode) { try { // Read EXI file byte[] buffer = File.ReadAllBytes(filename); if (!xmlMode) { // Analysis mode - show detailed information like C version Console.WriteLine($"File: {filename} ({buffer.Length} bytes)"); Console.Write("Raw hex data: "); int displayLength = Math.Min(buffer.Length, 32); for (int i = 0; i < displayLength; i++) { Console.Write($"{buffer[i]:X2} "); } if (buffer.Length > 32) Console.Write("..."); Console.WriteLine("\n"); // Analyze data structure AnalyzeDataStructure(buffer); } // Extract EXI body from V2G Transfer Protocol data byte[] exiBuffer = ExtractExiBody(buffer); if (exiBuffer.Length != buffer.Length && !xmlMode) { Console.WriteLine($"\n=== V2G Transfer Protocol Analysis ==="); Console.WriteLine($"Original size: {buffer.Length} bytes"); Console.WriteLine($"EXI body size: {exiBuffer.Length} bytes"); Console.WriteLine($"Stripped V2GTP header: {buffer.Length - exiBuffer.Length} bytes"); } // Decode EXI message DecodeResult result; if (xmlMode) { // Suppress debug output for XML-only mode using (var sw = new StringWriter()) { var originalOut = Console.Out; Console.SetOut(sw); try { result = V2GMessageProcessor.DecodeExiMessage(exiBuffer); } finally { Console.SetOut(originalOut); } } } else { result = V2GMessageProcessor.DecodeExiMessage(exiBuffer); } if (result.Success) { if (xmlMode) { // XML decode mode - output clean XML only Console.Write(result.XmlOutput); } else { // Analysis mode - show detailed analysis Console.WriteLine(result.AnalysisOutput); Console.WriteLine(result.XmlOutput); // Also show XML in analysis mode } return 0; } else { Console.Error.WriteLine($"Error decoding EXI: {result.ErrorMessage}"); return -1; } } catch (Exception ex) { Console.Error.WriteLine($"Error processing file: {ex.Message}"); return -1; } } private static void AnalyzeDataStructure(byte[] buffer) { Console.WriteLine("=== Data Structure Analysis ==="); Console.WriteLine($"Total size: {buffer.Length} bytes"); if (buffer.Length >= 4) { uint firstFourBytes = (uint)((buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]); Console.WriteLine($"First 4 bytes: 0x{firstFourBytes:X8}"); } // Determine protocol type and analyze if (buffer.Length >= 8 && buffer[0] == V2G_PROTOCOL_VERSION && buffer[1] == V2G_INV_PROTOCOL_VERSION) { Console.WriteLine("Protocol: V2G Transfer Protocol detected"); AnalyzeV2GTPHeader(buffer); } else if (buffer.Length >= 2 && ((buffer[0] << 8) | buffer[1]) == EXI_START_PATTERN) { Console.WriteLine("Protocol: Direct EXI format"); AnalyzeEXIStructure(buffer, 0); } else { Console.WriteLine("Protocol: Unknown format - attempting EXI detection"); // Check for EXI start pattern anywhere in the buffer for (int i = 0; i <= buffer.Length - 2; i++) { ushort pattern = (ushort)((buffer[i] << 8) | buffer[i + 1]); if (pattern == EXI_START_PATTERN) { Console.WriteLine($"EXI start pattern (0x{EXI_START_PATTERN:X4}) found at offset: {i}"); AnalyzeEXIStructure(buffer, i); break; } } } Console.WriteLine(); } private static void AnalyzeV2GTPHeader(byte[] buffer) { if (buffer.Length < 8) return; Console.WriteLine("\n--- V2G Transfer Protocol Header ---"); Console.WriteLine($"Version: 0x{buffer[0]:X2}"); Console.WriteLine($"Inverse Version: 0x{buffer[1]:X2}"); ushort payloadType = (ushort)((buffer[2] << 8) | buffer[3]); Console.WriteLine($"Payload Type: 0x{payloadType:X4} ({GetPayloadTypeDescription(payloadType)})"); uint payloadLength = (uint)((buffer[4] << 24) | (buffer[5] << 16) | (buffer[6] << 8) | buffer[7]); Console.WriteLine($"Payload Length: {payloadLength} bytes"); if (8 + payloadLength == buffer.Length) { Console.WriteLine("✓ V2GTP header is valid (payload length matches)"); } else { Console.WriteLine($"⚠ V2GTP header mismatch (expected {8 + payloadLength}, got {buffer.Length})"); } // Analyze EXI payload if present if (buffer.Length > 8) { AnalyzeEXIStructure(buffer, 8); } } private static void AnalyzeEXIStructure(byte[] buffer, int offset) { if (offset + 4 > buffer.Length) return; Console.WriteLine("\n--- EXI Structure Analysis ---"); Console.WriteLine($"EXI data starts at offset: {offset}"); Console.WriteLine($"EXI payload size: {buffer.Length - offset} bytes"); // EXI Header analysis if (offset + 4 <= buffer.Length) { Console.WriteLine($"EXI Magic: 0x{buffer[offset]:X2} (expected 0x80)"); Console.WriteLine($"Document Choice: 0x{buffer[offset + 1]:X2}"); Console.WriteLine($"Grammar State: 0x{buffer[offset + 2]:X2} 0x{buffer[offset + 3]:X2}"); } // SessionID analysis (if present after EXI header) if (offset + 12 <= buffer.Length) { Console.WriteLine("\n--- SessionID Analysis ---"); Console.Write("SessionID bytes: "); for (int i = offset + 4; i < offset + 12 && i < buffer.Length; i++) { Console.Write($"{buffer[i]:X2} "); } Console.WriteLine(); // Try to decode SessionID as ASCII if reasonable if (IsReadableAscii(buffer, offset + 4, 8)) { string sessionId = System.Text.Encoding.ASCII.GetString(buffer, offset + 4, 8); Console.WriteLine($"SessionID (ASCII): \"{sessionId}\""); } } // Predict message type based on patterns PredictMessageType(buffer, offset); } private static void PredictMessageType(byte[] buffer, int offset) { Console.WriteLine("\n--- Message Type Prediction ---"); // Look for body choice pattern (around offset 12-16 typically) for (int i = offset + 8; i < Math.Min(offset + 20, buffer.Length); i++) { byte b = buffer[i]; // Body choice is typically encoded in specific patterns if ((b & 0xF0) == 0xD0) // Common pattern for body choices { int bodyChoice = (b >> 1) & 0x1F; // Extract 5-bit choice Console.WriteLine($"Possible Body Choice at offset {i}: {bodyChoice} ({GetMessageTypeDescription(bodyChoice)})"); } } } private static bool IsReadableAscii(byte[] buffer, int offset, int length) { for (int i = 0; i < length && offset + i < buffer.Length; i++) { byte b = buffer[offset + i]; if (b < 32 || b > 126) // Not printable ASCII return false; } return true; } private static string GetPayloadTypeDescription(ushort payloadType) { return payloadType switch { V2G_PAYLOAD_ISO_DIN_SAP => "ISO 15118-2/DIN/SAP", V2G_PAYLOAD_ISO2 => "ISO 15118-20", _ => "Unknown" }; } private static string GetMessageTypeDescription(int bodyChoice) { return bodyChoice switch { 0 => "SessionSetupReq", 1 => "SessionSetupRes", 2 => "ServiceDiscoveryReq", 3 => "ServiceDiscoveryRes", 4 => "ServiceDetailReq", 5 => "ServiceDetailRes", 6 => "PaymentServiceSelectionReq", 7 => "PaymentServiceSelectionRes", 8 => "AuthorizationReq", 9 => "AuthorizationRes", 10 => "ChargeParameterDiscoveryReq", 11 => "ChargeParameterDiscoveryRes", 12 => "PowerDeliveryReq", 13 => "CurrentDemandReq", 14 => "CurrentDemandRes", 15 => "PowerDeliveryRes", 16 => "ChargingStatusReq", 17 => "ChargingStatusRes", 18 => "SessionStopReq", 19 => "SessionStopRes", _ => $"Unknown ({bodyChoice})" }; } private static byte[] ExtractExiBody(byte[] inputData) { if (inputData.Length < 8) { // Too small for V2G TP header, assume it's pure EXI return inputData; } // Check for V2GTP header: Version(1) + Inv.Version(1) + PayloadType(2) + PayloadLength(4) if (inputData[0] == V2G_PROTOCOL_VERSION && inputData[1] == V2G_INV_PROTOCOL_VERSION) { // Extract payload type and length ushort payloadType = (ushort)((inputData[2] << 8) | inputData[3]); uint payloadLength = (uint)((inputData[4] << 24) | (inputData[5] << 16) | (inputData[6] << 8) | inputData[7]); if (payloadType == V2G_PAYLOAD_ISO_DIN_SAP || payloadType == V2G_PAYLOAD_ISO2) { if (8 + payloadLength <= inputData.Length) { byte[] result = new byte[payloadLength]; Array.Copy(inputData, 8, result, 0, (int)payloadLength); return result; } } } // Not V2GTP format, return as-is return inputData; } private static int HandleStdinDecode() { try { // Read hex string from stdin (like VC2022) string hexInput = Console.In.ReadToEnd().Trim(); // Remove spaces and convert hex to bytes hexInput = hexInput.Replace(" ", "").Replace("\n", "").Replace("\r", ""); if (hexInput.Length % 2 != 0) { Console.Error.WriteLine("Error: Invalid hex string length"); return -1; } byte[] exiData = new byte[hexInput.Length / 2]; for (int i = 0; i < exiData.Length; i++) { exiData[i] = Convert.ToByte(hexInput.Substring(i * 2, 2), 16); } // Decode and output XML var result = V2GMessageProcessor.DecodeExiMessage(exiData); if (result.Success) { Console.Write(result.XmlOutput); return 0; } else { Console.Error.WriteLine($"Error: {result.ErrorMessage}"); return -1; } } catch (Exception ex) { Console.Error.WriteLine($"Error reading from stdin: {ex.Message}"); return -1; } } private static int HandleStdinEncode() { try { // Read XML from stdin (like VC2022) string xmlInput = Console.In.ReadToEnd(); // Encode XML to EXI var exiData = V2GMessageProcessor.EncodeXmlToExi(xmlInput); if (exiData == null || exiData.Length == 0) { Console.Error.WriteLine("Error encoding XML to EXI"); return -1; } // Check if output is redirected bool isRedirected = Console.IsOutputRedirected; if (isRedirected) { // Binary output for redirection using (var stdout = Console.OpenStandardOutput()) { stdout.Write(exiData, 0, exiData.Length); stdout.Flush(); } } else { // Hex string output for console display Console.Write(BitConverter.ToString(exiData).Replace("-", "")); } return 0; } catch (Exception ex) { Console.Error.WriteLine($"Error reading from stdin: {ex.Message}"); return -1; } } } }