- Auto-detect XML files for encoding, other files for decoding/analysis - Enhanced structure analysis for both .NET and VC versions - Added V2GTP header analysis and EXI structure breakdown - Added message type prediction based on body choice patterns - Improved SessionID analysis with ASCII decoding - Updated usage messages to reflect auto-detection capabilities 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
529 lines
21 KiB
C#
529 lines
21 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
} |