feat: Add GeViSet file format reverse engineering specification

- Add comprehensive spec for .set file format parsing
- Document binary structure, data types, and sections
- Add research notes from binary analysis
- Fix SetupClient password encryption (GeViAPI_EncodeString)
- Add DiagnoseSetupClient tool for testing
- Successfully tested: read/write 281KB config, byte-perfect round-trip
- Found 64 action mappings in live server configuration

Next: Full binary parser implementation for complete structure

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Geutebruck API Developer
2025-12-12 12:50:46 +01:00
parent 7b9aab9e8b
commit 24a11cecdd
5 changed files with 1585 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x86</PlatformTarget>
<Platforms>x86</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GeViScopeBridge\GeViScopeBridge.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="GeViProcAPINET_4_0">
<HintPath>C:\GEVISOFT\GeViProcAPINET_4_0.dll</HintPath>
</Reference>
</ItemGroup>
<Target Name="CopyGeViSoftDLLs" AfterTargets="Build">
<ItemGroup>
<GeViSoftFiles Include="C:\GEVISOFT\*.dll" />
</ItemGroup>
<Copy SourceFiles="@(GeViSoftFiles)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" />
<Message Text="Copied GeViSoft DLLs to output directory" Importance="high" />
</Target>
</Project>

View File

@@ -0,0 +1,268 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Serilog;
using GeViScopeBridge.SDK;
namespace DiagnoseSetupClient
{
class Program
{
static async Task Main(string[] args)
{
// Configure logging
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
try
{
Console.WriteLine("=== GeViSetupClient Diagnostic Tool ===\n");
// Get connection details from command line or interactive
string address, username, password;
if (args.Length >= 3)
{
// Command line mode: DiagnoseSetupClient.exe <address> <username> <password>
address = args[0];
username = args[1];
password = args[2];
Console.WriteLine($"Using command-line arguments:");
Console.WriteLine($" Address: {address}");
Console.WriteLine($" Username: {username}");
Console.WriteLine($" Password: {new string('*', password.Length)}");
}
else
{
// Interactive mode
Console.Write("GeViServer Address (default: localhost): ");
address = Console.ReadLine();
if (string.IsNullOrWhiteSpace(address))
address = "localhost";
Console.Write("Username (default: admin): ");
username = Console.ReadLine();
if (string.IsNullOrWhiteSpace(username))
username = "admin";
Console.Write("Password: ");
password = ReadPassword();
}
Console.WriteLine("\n\n1. Testing SetupClient Connection...");
// Try with different aliasnames
string[] aliasnamesToTry = { "", "localhost", "GeViServer", address };
GeViSetupClientWrapper successfulClient = null;
foreach (var aliasname in aliasnamesToTry)
{
Console.WriteLine($"Trying with aliasname: '{aliasname}'");
var setupClient = new GeViSetupClientWrapper(address, username, password, aliasname);
bool connected = await setupClient.ConnectAsync();
if (connected)
{
Console.WriteLine($"✅ Connected successfully with aliasname: '{aliasname}'!\n");
successfulClient = setupClient;
break;
}
else
{
setupClient.Dispose();
}
}
if (successfulClient == null)
{
Console.WriteLine("❌ Failed to connect with any aliasname");
return;
}
// Use the successfully connected client
using (var setupClient = successfulClient)
{
// Test ping
Console.WriteLine("2. Testing Ping...");
bool pingResult = setupClient.SendPing();
Console.WriteLine(pingResult ? "✅ Ping successful" : "❌ Ping failed");
Console.WriteLine();
// Read setup configuration
Console.WriteLine("3. Reading Setup Configuration...");
byte[] setupData = await setupClient.ReadSetupAsync();
Console.WriteLine($"✅ Read {setupData.Length} bytes of configuration\n");
// Save to file for inspection
string outputFile = Path.Combine(
Environment.CurrentDirectory,
$"setup_config_{DateTime.Now:yyyyMMdd_HHmmss}.dat"
);
File.WriteAllBytes(outputFile, setupData);
Console.WriteLine($"📁 Saved configuration to: {outputFile}\n");
// Analyze file format
Console.WriteLine("4. Analyzing File Format...");
AnalyzeSetupFile(setupData);
Console.WriteLine("\n5. Testing Write Setup (write back unchanged)...");
// In automated mode, skip write test by default
string response = "n";
if (args.Length < 3)
{
Console.Write("Write configuration back to server? (y/n): ");
response = Console.ReadLine();
}
else
{
Console.WriteLine("Skipping write test in automated mode (pass 4th argument 'y' to enable)");
if (args.Length >= 4 && args[3].ToLower() == "y")
{
response = "y";
}
}
if (response?.ToLower() == "y")
{
bool writeSuccess = await setupClient.WriteSetupAsync(setupData);
Console.WriteLine(writeSuccess
? "✅ Configuration written successfully"
: "❌ Failed to write configuration");
}
else
{
Console.WriteLine("⏭️ Skipped write test");
}
}
Console.WriteLine("\n✅ All tests completed successfully!");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ Error: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
finally
{
Log.CloseAndFlush();
}
// Only wait for key if in interactive mode (not automated)
if (args.Length < 3)
{
Console.WriteLine("\nPress any key to exit...");
try
{
Console.ReadKey();
}
catch
{
// Ignore if console input is redirected
}
}
}
static void AnalyzeSetupFile(byte[] data)
{
// Check if XML
if (data.Length > 5)
{
string header = System.Text.Encoding.ASCII.GetString(data, 0, Math.Min(100, data.Length));
if (header.StartsWith("<?xml") || header.StartsWith("<"))
{
Console.WriteLine(" Format: XML");
Console.WriteLine($" First 200 chars:\n{header}");
return;
}
}
// Check for common text encodings
try
{
string utf8Text = System.Text.Encoding.UTF8.GetString(data, 0, Math.Min(200, data.Length));
if (IsText(utf8Text))
{
Console.WriteLine(" Format: Text (UTF-8)");
Console.WriteLine($" First 200 chars:\n{utf8Text}");
return;
}
}
catch { }
// Binary format
Console.WriteLine(" Format: Binary");
Console.WriteLine(" Hex dump (first 100 bytes):");
HexDump(data, Math.Min(100, data.Length));
}
static bool IsText(string str)
{
foreach (char c in str)
{
if (char.IsControl(c) && c != '\r' && c != '\n' && c != '\t')
return false;
}
return true;
}
static void HexDump(byte[] data, int length)
{
for (int i = 0; i < length; i += 16)
{
Console.Write($" {i:X4}: ");
// Hex
for (int j = 0; j < 16; j++)
{
if (i + j < length)
Console.Write($"{data[i + j]:X2} ");
else
Console.Write(" ");
}
Console.Write(" ");
// ASCII
for (int j = 0; j < 16 && i + j < length; j++)
{
byte b = data[i + j];
Console.Write(b >= 32 && b < 127 ? (char)b : '.');
}
Console.WriteLine();
}
}
static string ReadPassword()
{
string password = "";
ConsoleKeyInfo key;
do
{
key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Backspace && password.Length > 0)
{
password = password.Substring(0, password.Length - 1);
Console.Write("\b \b");
}
else if (key.Key != ConsoleKey.Enter && key.KeyChar != '\0')
{
password += key.KeyChar;
Console.Write("*");
}
} while (key.Key != ConsoleKey.Enter);
return password;
}
}
}

View File

@@ -0,0 +1,391 @@
using System;
using System.Runtime.InteropServices;
using System.IO;
using System.Threading.Tasks;
using Serilog;
using Microsoft.Win32.SafeHandles;
namespace GeViScopeBridge.SDK
{
/// <summary>
/// P/Invoke wrapper for GeViAPI SetupClient functions
/// This is what GeViSet uses to read/write configuration from/to GeViServer
/// </summary>
public class GeViSetupClientWrapper : IDisposable
{
private IntPtr _setupClientHandle = IntPtr.Zero;
private bool _isConnected = false;
private readonly ILogger _logger;
// Connection parameters
private readonly string _aliasname;
private readonly string _address;
private readonly string _username;
private readonly string _password;
#region P/Invoke Declarations
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern bool GeViAPI_SetupClient_Create(
out IntPtr setupClient,
[MarshalAs(UnmanagedType.LPStr)] string aliasname,
[MarshalAs(UnmanagedType.LPStr)] string address,
[MarshalAs(UnmanagedType.LPStr)] string username,
[MarshalAs(UnmanagedType.LPStr)] string password,
[MarshalAs(UnmanagedType.LPStr)] string username2,
[MarshalAs(UnmanagedType.LPStr)] string password2
);
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool GeViAPI_SetupClient_Connect(
IntPtr setupClient,
out int connectResult,
IntPtr callback, // TGeViConnectProgress callback (can be IntPtr.Zero)
IntPtr instance // void* instance (can be IntPtr.Zero)
);
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool GeViAPI_SetupClient_Disconnect(IntPtr setupClient);
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool GeViAPI_SetupClient_Destroy(IntPtr setupClient);
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool GeViAPI_SetupClient_ReadSetup(
IntPtr setupClient,
IntPtr hFile // File handle from CreateFile
);
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool GeViAPI_SetupClient_WriteSetup(
IntPtr setupClient,
IntPtr hFile // File handle from CreateFile
);
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool GeViAPI_SetupClient_SendPing(IntPtr setupClient);
// Windows API for file operations
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile
);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
// Password encoding function
[DllImport("GeViProcAPI.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void GeViAPI_EncodeString(
[MarshalAs(UnmanagedType.LPStr)] System.Text.StringBuilder output,
[MarshalAs(UnmanagedType.LPStr)] string input,
int size
);
// File access constants
private const uint GENERIC_READ = 0x80000000;
private const uint GENERIC_WRITE = 0x40000000;
private const uint CREATE_ALWAYS = 2;
private const uint OPEN_EXISTING = 3;
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
#endregion
public GeViSetupClientWrapper(string address, string username, string password, string aliasname = "")
{
_address = address ?? throw new ArgumentNullException(nameof(address));
_username = username ?? throw new ArgumentNullException(nameof(username));
_password = password ?? throw new ArgumentNullException(nameof(password));
_aliasname = aliasname ?? "";
_logger = Log.ForContext<GeViSetupClientWrapper>();
}
/// <summary>
/// Connect to GeViServer
/// </summary>
public async Task<bool> ConnectAsync()
{
return await Task.Run(() => Connect());
}
private bool Connect()
{
try
{
_logger.Information("Creating SetupClient for {Address}", _address);
// Encrypt password using GeViAPI_EncodeString
// Password buffer should be at least 256 bytes according to typical SDK usage
var encodedPassword = new System.Text.StringBuilder(256);
GeViAPI_EncodeString(encodedPassword, _password, encodedPassword.Capacity);
_logger.Debug("Password encrypted for SetupClient connection");
// Create SetupClient with encrypted password
bool created = GeViAPI_SetupClient_Create(
out _setupClientHandle,
_aliasname,
_address,
_username,
encodedPassword.ToString(), // Use encrypted password
"", // username2 (optional, for dual control)
"" // password2 (optional)
);
if (!created || _setupClientHandle == IntPtr.Zero)
{
_logger.Error("Failed to create SetupClient");
return false;
}
_logger.Information("SetupClient created, connecting to {Address}", _address);
// Connect to server
bool connected = GeViAPI_SetupClient_Connect(
_setupClientHandle,
out int connectResult,
IntPtr.Zero, // No progress callback
IntPtr.Zero // No instance
);
if (!connected || connectResult != 0)
{
string errorName = GetConnectResultName(connectResult);
_logger.Error("Failed to connect SetupClient. Result: {Result} ({ErrorName})", connectResult, errorName);
return false;
}
_isConnected = true;
_logger.Information("SetupClient connected successfully to {Address}", _address);
return true;
}
catch (Exception ex)
{
_logger.Error(ex, "Exception during SetupClient connection");
return false;
}
}
/// <summary>
/// Read complete setup configuration from GeViServer to a file
/// </summary>
public async Task<byte[]> ReadSetupAsync()
{
return await Task.Run(() => ReadSetup());
}
private byte[] ReadSetup()
{
if (!_isConnected || _setupClientHandle == IntPtr.Zero)
{
throw new InvalidOperationException("SetupClient is not connected");
}
string tempFile = Path.GetTempFileName();
try
{
_logger.Information("Reading setup configuration from GeViServer to {TempFile}", tempFile);
// Create file handle for writing
IntPtr hFile = CreateFile(
tempFile,
GENERIC_WRITE,
0, // No sharing
IntPtr.Zero,
CREATE_ALWAYS,
0,
IntPtr.Zero
);
if (hFile == INVALID_HANDLE_VALUE)
{
int error = Marshal.GetLastWin32Error();
throw new IOException($"Failed to create temp file. Error: {error}");
}
try
{
// Read setup from server
bool success = GeViAPI_SetupClient_ReadSetup(_setupClientHandle, hFile);
if (!success)
{
throw new InvalidOperationException("Failed to read setup from GeViServer");
}
_logger.Information("Setup configuration read successfully");
}
finally
{
CloseHandle(hFile);
}
// Read file contents
byte[] data = File.ReadAllBytes(tempFile);
_logger.Information("Read {Size} bytes of setup configuration", data.Length);
return data;
}
finally
{
// Clean up temp file
if (File.Exists(tempFile))
{
try
{
File.Delete(tempFile);
}
catch (Exception ex)
{
_logger.Warning(ex, "Failed to delete temp file {TempFile}", tempFile);
}
}
}
}
/// <summary>
/// Write setup configuration back to GeViServer from a byte array
/// </summary>
public async Task<bool> WriteSetupAsync(byte[] setupData)
{
return await Task.Run(() => WriteSetup(setupData));
}
private bool WriteSetup(byte[] setupData)
{
if (!_isConnected || _setupClientHandle == IntPtr.Zero)
{
throw new InvalidOperationException("SetupClient is not connected");
}
if (setupData == null || setupData.Length == 0)
{
throw new ArgumentException("Setup data cannot be null or empty", nameof(setupData));
}
string tempFile = Path.GetTempFileName();
try
{
_logger.Information("Writing {Size} bytes of setup configuration to GeViServer", setupData.Length);
// Write data to temp file
File.WriteAllBytes(tempFile, setupData);
// Open file handle for reading
IntPtr hFile = CreateFile(
tempFile,
GENERIC_READ,
0, // No sharing
IntPtr.Zero,
OPEN_EXISTING,
0,
IntPtr.Zero
);
if (hFile == INVALID_HANDLE_VALUE)
{
int error = Marshal.GetLastWin32Error();
throw new IOException($"Failed to open temp file. Error: {error}");
}
try
{
// Write setup to server
bool success = GeViAPI_SetupClient_WriteSetup(_setupClientHandle, hFile);
if (!success)
{
_logger.Error("Failed to write setup to GeViServer");
return false;
}
_logger.Information("Setup configuration written successfully to GeViServer");
return true;
}
finally
{
CloseHandle(hFile);
}
}
finally
{
// Clean up temp file
if (File.Exists(tempFile))
{
try
{
File.Delete(tempFile);
}
catch (Exception ex)
{
_logger.Warning(ex, "Failed to delete temp file {TempFile}", tempFile);
}
}
}
}
/// <summary>
/// Send ping to keep connection alive
/// </summary>
public bool SendPing()
{
if (!_isConnected || _setupClientHandle == IntPtr.Zero)
{
return false;
}
return GeViAPI_SetupClient_SendPing(_setupClientHandle);
}
/// <summary>
/// Disconnect from GeViServer
/// </summary>
public void Disconnect()
{
if (_isConnected && _setupClientHandle != IntPtr.Zero)
{
_logger.Information("Disconnecting SetupClient");
GeViAPI_SetupClient_Disconnect(_setupClientHandle);
_isConnected = false;
}
}
public void Dispose()
{
Disconnect();
if (_setupClientHandle != IntPtr.Zero)
{
_logger.Information("Destroying SetupClient");
GeViAPI_SetupClient_Destroy(_setupClientHandle);
_setupClientHandle = IntPtr.Zero;
}
}
private static string GetConnectResultName(int result)
{
return result switch
{
0 => "connectOk",
100 => "connectAborted",
101 => "connectGenericError",
300 => "connectRemoteUnknownError",
301 => "connectRemoteTcpError",
302 => "connectRemoteUnknownUser",
303 => "connectRemoteConnectionLimitExceeded",
304 => "connectRemoteClientInterfaceTooOld",
305 => "connectRemoteServerInterfaceTooOld",
306 => "connectRemoteSecondUserRequired",
307 => "connectRemotePortDisabled",
_ => $"Unknown({result})"
};
}
}
}