using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace V2GProtocol { class Program { static void Main(string[] args) { // 헤더는 도움말 또는 분석 모드에서만 표시 (인코드/디코드 작업에서는 표시하지 않음) bool showHeader = false; if (args.Length == 0) { showHeader = true; } else if (args.Length > 0) { string firstArg = args[0].ToLower(); // 도움말 명령인지 확인 if (firstArg == "--help" || firstArg == "-h") { showHeader = true; } // 분석 모드용 파일인지 확인 (옵션 없이 .dump/.txt만) else if (File.Exists(args[0]) && args.Length == 1) { string extension = Path.GetExtension(args[0]).ToLower(); // 분석 모드에서만 헤더 표시 (.dump/.txt) // 자동 변환에서는 표시하지 않음 (.hex, .xml) if (extension == ".dump" || extension == ".txt") { showHeader = true; } // .hex and .xml are auto-conversion, no header } } if (showHeader) { Console.WriteLine("=== V2G Transfer Protocol Decoder/Encoder"); Console.WriteLine("=== ISO 15118-2 / DIN SPEC 70121 / SAP Protocol"); Console.WriteLine("=== made by [tindevil82@gmail.com]"); Console.WriteLine(); } try { if (args.Length == 0) { ShowUsage(); return; } var command = args[0].ToLower(); // 첫 번째 인자가 옵션인 경우 (--decode, --encode, --help) if (command.StartsWith("--") || command.StartsWith("-")) { switch (command) { case "--decode": case "-d": HandleDecodeCommand(args); break; case "--encode": case "-e": HandleEncodeCommand(args); break; case "--help": case "-h": ShowUsage(); break; default: Console.WriteLine($"Error: Unknown option: {args[0]}"); ShowUsage(); break; } } // 두 번째 인자가 옵션인 경우 (filename --decode, filename --encode) else if (args.Length >= 2 && (args[1].ToLower() == "--decode" || args[1].ToLower() == "-d")) { // filename --decode 형태 - 나머지 인자들도 전달 var newArgs = new List { args[1], args[0] }; for (int i = 2; i < args.Length; i++) newArgs.Add(args[i]); HandleDecodeCommand(newArgs.ToArray()); } else if (args.Length >= 2 && (args[1].ToLower() == "--encode" || args[1].ToLower() == "-e")) { // filename --encode 형태 - 나머지 인자들도 전달 var newArgs = new List { args[1], args[0] }; for (int i = 2; i < args.Length; i++) newArgs.Add(args[i]); HandleEncodeCommand(newArgs.ToArray()); } // 파일명만 전달된 경우 (분석 모드) else if (File.Exists(args[0])) { // 파일 확장자를 확인하여 기본 동작 결정 string extension = Path.GetExtension(args[0]).ToLower(); if (extension == ".dump" || extension == ".txt") { // 헥스 덤프 파일 - 전체 분석 모드 사용 AnalyzeFile(args[0]); } else if (extension == ".hex") { // .hex 파일 - 자동으로 XML로 디코드 var autoArgs = new List { "--decode", args[0] }; // 나머지 인자 추가 (-out 등) for (int i = 1; i < args.Length; i++) autoArgs.Add(args[i]); HandleDecodeCommand(autoArgs.ToArray()); } else if (extension == ".xml") { // .xml 파일 - 자동으로 EXI로 인코드 var autoArgs = new List { "--encode", args[0] }; // 나머지 인자 추가 (-out 등) for (int i = 1; i < args.Length; i++) autoArgs.Add(args[i]); HandleEncodeCommand(autoArgs.ToArray()); } else { // 알 수 없는 확장자 - 명시적 옵션 필요 Console.WriteLine($"Error: Unknown file extension '{extension}'. Please specify operation:"); Console.WriteLine($" {Path.GetFileName(args[0])} --decode # To decode as EXI to XML"); Console.WriteLine($" {Path.GetFileName(args[0])} --encode # To encode as XML to EXI"); Console.WriteLine($" OR use: --decode {args[0]} / --encode {args[0]}"); return; } } else { Console.WriteLine($"Error: Unknown option or file not found: {args[0]}"); //ShowUsage(); } } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } static void ShowUsage() { Console.WriteLine("Usage:"); Console.WriteLine(" V2GDecoder.exe [file.dump/.txt] # Analyze hex dump file"); Console.WriteLine(" V2GDecoder.exe [file.hex] # Auto-decode hex to XML"); Console.WriteLine(" V2GDecoder.exe [file.xml] # Auto-encode XML to EXI"); Console.WriteLine(" V2GDecoder.exe --decode # Decode EXI to XML"); Console.WriteLine(" V2GDecoder.exe --encode # Encode XML to EXI"); Console.WriteLine(" V2GDecoder.exe --decode [-out file] # Force decode operation"); Console.WriteLine(" V2GDecoder.exe --encode [-out file] # Force encode operation"); Console.WriteLine(" V2GDecoder.exe --help # Show this help"); Console.WriteLine(); Console.WriteLine("File Extension Auto-Processing:"); Console.WriteLine(" .dump, .txt → Full analysis mode (network + V2G message)"); Console.WriteLine(" .hex → Auto-decode to XML"); Console.WriteLine(" .xml → Auto-encode to EXI hex"); Console.WriteLine(" Other → Requires --decode or --encode option"); Console.WriteLine(); Console.WriteLine("Pipe Usage:"); Console.WriteLine(" type file.xml | V2GDecoder.exe --encode # Pipe XML file to encode"); Console.WriteLine(" echo \"hex_string\" | V2GDecoder.exe --decode # Pipe hex string to decode"); Console.WriteLine("Example:"); Console.WriteLine("V2GDecoder.exe 01FE80010000001E809802104142423030303831908C0C0C0E0C51E020256968C0C0C0C0C080 --decode"); Console.WriteLine(); } static string GetOutputFilename(string[] args) { for (int i = 0; i < args.Length - 1; i++) { if (args[i].ToLower() == "-out" || args[i].ToLower() == "-o") { return args[i + 1]; } } return null; } static void HandleDecodeCommand(string[] args) { string input; byte[] exiBytes; string outputFile = GetOutputFilename(args); // 파일/문자열 입력을 위한 충분한 인자가 있는지 확인 if (args.Length >= 2) { // 명령줄 인자가 있으므로 사용 input = args[1]; } else if (Console.IsInputRedirected) { // 충분한 인자가 없으므로 stdin(파이프)에서 입력이 오는지 확인 input = Console.In.ReadToEnd().Trim(); if (string.IsNullOrEmpty(input)) { Console.WriteLine("Error: No input data received from stdin"); return; } outputFile = null; // stdin의 경우 콘솔 출력 강제 } else { Console.WriteLine("Error: EXI hex string or file required for decode operation"); Console.WriteLine("Usage: V2GDecoder.exe --decode [-out output_file]"); return; } // 입력이 파일인지 확인 if (File.Exists(input)) { // 다양한 파일 확장자 처리 string extension = Path.GetExtension(input).ToLower(); if (extension == ".dump") { // 화살표(→)가 있는 헥스 덤프 파일 형식 byte[] rawData = V2GDecoder.ParseHexFile(input); // 헥스 덤프에서 V2G 메시지 찾기 var v2gMessage = ExtractV2GMessageFromHexDump(rawData); if (v2gMessage != null) { exiBytes = v2gMessage; } else { // 원시 데이터를 직접 디코드 시도 (V2G 헤더 없는 순수 EXI일 수 있음) exiBytes = rawData; } } else if (extension == ".hex") { // 바이너리 hex 파일 exiBytes = File.ReadAllBytes(input); } else if (extension == ".txt") { // .txt 파일에 대한 레거시 지원 - 컨텐츠 형식 확인 string fileContent = File.ReadAllText(input).Trim(); if (fileContent.Contains("→")) { // 헥스 덤프 형식 byte[] rawData = V2GDecoder.ParseHexFile(input); var v2gMessage = ExtractV2GMessageFromHexDump(rawData); if (v2gMessage != null) { exiBytes = v2gMessage; } else { exiBytes = rawData; } } else { // 순수 헥스 fileContent = fileContent.Replace(" ", "").Replace("-", "").Replace("0x", "").Replace("\n", "").Replace("\r", ""); exiBytes = Helper.FromHexString(fileContent); } } else { // 알 수 없는 확장자, 순수 헥스로 시도 string fileContent = File.ReadAllText(input).Trim(); fileContent = fileContent.Replace(" ", "").Replace("-", "").Replace("0x", "").Replace("\n", "").Replace("\r", ""); exiBytes = Helper.FromHexString(fileContent); } } else { // 직접 헥스 문자열 입력 string exiHexString = input.Replace(" ", "").Replace("-", "").Replace("0x", ""); if (exiHexString.Length % 2 != 0) { Console.WriteLine("Error: Invalid hex string (odd number of characters)"); return; } exiBytes = Helper.FromHexString(exiHexString); } try { // EXI를 XML로 디코드 - XML만 출력 var decodedXml = V2GDecoder.DecodeEXIToXML(exiBytes); if (outputFile != null) { // 파일로 저장 File.WriteAllText(outputFile, decodedXml); Console.WriteLine($"Decoded XML saved to: {outputFile}"); } else { // 콘솔로 출력 Console.WriteLine(decodedXml); } } catch (Exception ex) { Console.WriteLine($"Error decoding EXI: {ex.Message}"); } } static byte[] ExtractV2GMessageFromHexDump(byte[] data) { // V2G Transfer Protocol 시그니처 찾기 (0x01FE) for (int i = 0; i < data.Length - 8; i++) { if (data[i] == 0x01 && data[i + 1] == 0xFE) { // 페이로드 길이 가져오기 uint payloadLength = (uint)((data[i + 4] << 24) | (data[i + 5] << 16) | (data[i + 6] << 8) | data[i + 7]); // EXI 페이로드 추출 if (i + 8 + payloadLength <= data.Length) { byte[] exiPayload = new byte[payloadLength]; Array.Copy(data, i + 8, exiPayload, 0, (int)payloadLength); return exiPayload; } } } return null; } static void HandleEncodeCommand(string[] args) { string xmlInput; string xmlContent; string outputFile = GetOutputFilename(args); // 파일/문자열 입력을 위한 충분한 인자가 있는지 확인 if (args.Length >= 2) { // 명령줄 인자가 있으므로 사용 xmlInput = args[1]; // 입력이 파일인지 확인 if (File.Exists(xmlInput)) { xmlContent = File.ReadAllText(xmlInput); } else { xmlContent = xmlInput; } } else if (Console.IsInputRedirected) { // 충분한 인자가 없으므로 stdin(파이프)에서 입력이 오는지 확인 xmlContent = Console.In.ReadToEnd().Trim(); if (string.IsNullOrEmpty(xmlContent)) { Console.WriteLine("Error: No input data received from stdin"); return; } outputFile = null; // stdin의 경우 콘솔 출력 강제 } else { Console.WriteLine("Error: XML string or file required for encode operation"); Console.WriteLine("Usage: V2GDecoder.exe --encode [-out output_file]"); return; } try { // XML을 EXI로 인코드 var exiBytes = V2GDecoder.EncodeXMLToEXI(xmlContent); if (outputFile != null) { // Save as binary file File.WriteAllBytes(outputFile, exiBytes); Console.WriteLine($"Encoded binary saved to: {outputFile} ({exiBytes.Length} bytes)"); // Also show hex string on console for reference var exiHexString = Helper.ToHexString(exiBytes); Console.WriteLine($"Hex: {exiHexString}"); } else { // Output hex string to console var exiHexString = Helper.ToHexString(exiBytes); Console.WriteLine(exiHexString); } } catch (Exception ex) { Console.WriteLine($"Error encoding XML: {ex.Message}"); } } static void AnalyzeFile(string dataFilePath) { if (!File.Exists(dataFilePath)) { Console.WriteLine($"Error: Data file not found: {dataFilePath}"); return; } Console.WriteLine($"Analyzing file: {dataFilePath}"); // 헥스 파일에서 바이너리 데이터 파싱 byte[] rawData = V2GDecoder.ParseHexFile(dataFilePath); Console.WriteLine($"Parsed {rawData.Length} bytes from hex file"); Console.WriteLine(); // 전체 데이터 헥스 덤프 Console.WriteLine("=== Raw Data Hex Dump ==="); Console.WriteLine(V2GDecoder.BytesToHex(rawData)); Console.WriteLine(); // V2G 메시지 디코딩 시도 Console.WriteLine("=== V2G Message Analysis ==="); // 전체 데이터에서 V2G 메시지 찾기 AnalyzeV2GMessages(rawData); // 네트워크 패킷 분석 (이더넷/IPv6/TCP 헤더 포함인 경우) AnalyzeNetworkPacket(rawData); } static void AnalyzeV2GMessages(byte[] data) { // V2G Transfer Protocol 시그니처 찾기 for (int i = 0; i < data.Length - 8; i++) { if (data[i] == 0x01 && data[i + 1] == 0xFE) // V2G TP Header { Console.WriteLine($"Potential V2G message found at offset 0x{i:X4}"); try { byte[] messageData = new byte[data.Length - i]; Array.Copy(data, i, messageData, 0, messageData.Length); var message = V2GDecoder.DecodeMessage(messageData); Console.WriteLine($" Version: 0x{message.Version:X2}"); Console.WriteLine($" Inverse Version: 0x{message.InverseVersion:X2}"); Console.WriteLine($" Payload Type: 0x{(ushort)message.PayloadType:X4} ({message.PayloadType})"); Console.WriteLine($" Payload Length: {message.PayloadLength} bytes"); Console.WriteLine($" Valid: {message.IsValid}"); Console.WriteLine(); if (message.IsValid && message.Payload != null) { Console.WriteLine("=== Decoded Message Content ==="); Console.WriteLine(message.DecodedContent); Console.WriteLine(); } } catch (Exception ex) { Console.WriteLine($" Error decoding message: {ex.Message}"); } } } } static void AnalyzeNetworkPacket(byte[] data) { Console.WriteLine("=== Network Packet Analysis ==="); if (data.Length < 54) // Minimum Ethernet + IPv6 + TCP header size { Console.WriteLine("Data too short for network packet analysis"); return; } try { // 이더넷 헤더 분석 (14 bytes) Console.WriteLine("Ethernet Header:"); Console.WriteLine($" Destination MAC: {string.Join(":", data.Take(6).Select(b => b.ToString("X2")))}"); Console.WriteLine($" Source MAC: {string.Join(":", data.Skip(6).Take(6).Select(b => b.ToString("X2")))}"); Console.WriteLine($" EtherType: 0x{(data[12] << 8 | data[13]):X4}"); ushort etherType = (ushort)(data[12] << 8 | data[13]); if (etherType == 0x86DD) // IPv6 { Console.WriteLine("\nIPv6 Header:"); int ipv6Offset = 14; byte versionTrafficClass = data[ipv6Offset]; Console.WriteLine($" Version: {(versionTrafficClass >> 4) & 0xF}"); Console.WriteLine($" Traffic Class: 0x{((versionTrafficClass & 0xF) << 4 | (data[ipv6Offset + 1] >> 4)):X2}"); ushort payloadLength = (ushort)(data[ipv6Offset + 4] << 8 | data[ipv6Offset + 5]); byte nextHeader = data[ipv6Offset + 6]; byte hopLimit = data[ipv6Offset + 7]; Console.WriteLine($" Payload Length: {payloadLength}"); Console.WriteLine($" Next Header: 0x{nextHeader:X2} ({(nextHeader == 6 ? "TCP" : "Other")})"); Console.WriteLine($" Hop Limit: {hopLimit}"); // IPv6 주소는 16바이트씩 byte[] srcAddr = new byte[16]; byte[] dstAddr = new byte[16]; Array.Copy(data, ipv6Offset + 8, srcAddr, 0, 16); Array.Copy(data, ipv6Offset + 24, dstAddr, 0, 16); Console.WriteLine($" Source Address: {string.Join(":", Enumerable.Range(0, 8).Select(i => $"{srcAddr[i*2]:X2}{srcAddr[i*2+1]:X2}"))}"); Console.WriteLine($" Destination Address: {string.Join(":", Enumerable.Range(0, 8).Select(i => $"{dstAddr[i*2]:X2}{dstAddr[i*2+1]:X2}"))}"); if (nextHeader == 6) // TCP { int tcpOffset = ipv6Offset + 40; if (data.Length > tcpOffset + 20) { Console.WriteLine("\nTCP Header:"); ushort srcPort = (ushort)(data[tcpOffset] << 8 | data[tcpOffset + 1]); ushort dstPort = (ushort)(data[tcpOffset + 2] << 8 | data[tcpOffset + 3]); Console.WriteLine($" Source Port: {srcPort}"); Console.WriteLine($" Destination Port: {dstPort}"); // V2G는 일반적으로 포트 15118을 사용 if (srcPort == 15118 || dstPort == 15118) { Console.WriteLine(" -> V2G Communication detected (port 15118)!"); } // TCP 데이터 시작 위치 계산 byte dataOffset = (byte)((data[tcpOffset + 12] >> 4) * 4); int tcpDataOffset = tcpOffset + dataOffset; if (tcpDataOffset < data.Length) { Console.WriteLine($"\nTCP Payload (starting at offset 0x{tcpDataOffset:X4}):"); byte[] tcpPayload = new byte[data.Length - tcpDataOffset]; Array.Copy(data, tcpDataOffset, tcpPayload, 0, tcpPayload.Length); // TCP 페이로드에서 V2G 메시지 찾기 if (tcpPayload.Length > 8 && tcpPayload[0] == 0x01 && tcpPayload[1] == 0xFE) { Console.WriteLine("V2G Message found in TCP payload!"); var v2gMessage = V2GDecoder.DecodeMessage(tcpPayload); Console.WriteLine(v2gMessage.DecodedContent); } else { Console.WriteLine("TCP Payload hex dump:"); Console.WriteLine(V2GDecoder.BytesToHex(tcpPayload)); } } } } } } catch (Exception ex) { Console.WriteLine($"Error analyzing network packet: {ex.Message}"); } } } }