Files
Qinventor/scripts/launcher/RuntimePath.cs
T

113 lines
6.0 KiB
C#

using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Win32.SafeHandles;
// Some upstream DXF readers still use narrow std::ifstream for resource files.
// Give the native runtime an ASCII executable path while retaining the real
// portable folder for settings, projects and QI_ROOT. No CAD files are copied.
static class RuntimePath
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern uint GetShortPathNameW(string longPath, StringBuilder shortPath, uint capacity);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr security,
uint creation, uint flags, IntPtr template);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeviceIoControl(SafeFileHandle file, uint control, byte[] input, uint inputSize,
IntPtr output, uint outputSize, out uint returned, IntPtr overlapped);
static bool IsAscii(string value) => value.All(c => c < 128);
static string? ShortAsciiPath(string path)
{
if (IsAscii(path)) return path;
var buffer = new StringBuilder(32768);
var length = GetShortPathNameW(path, buffer, (uint)buffer.Capacity);
return length > 0 && length < buffer.Capacity && IsAscii(buffer.ToString())
? buffer.ToString() : null;
}
public static string ForEngine(string root, string engine, bool allowShortPath = true)
{
if (allowShortPath && ShortAsciiPath(engine) is { } shortEngine) return shortEngine;
var relativeEngine = Path.GetRelativePath(root, engine);
if (relativeEngine.StartsWith(".." + Path.DirectorySeparatorChar) || Path.IsPathRooted(relativeEngine))
throw new IOException("Путь CAD-движка находится за пределами переносимой папки.");
var key = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(root.ToUpperInvariant())))[..32];
var candidates = new[]
{
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
Path.GetTempPath(),
Environment.GetFolderPath(Environment.SpecialFolder.CommonDocuments),
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
};
Exception? lastError = null;
foreach (var parent in candidates.Where(p => !string.IsNullOrWhiteSpace(p)).Distinct())
{
var asciiParent = ShortAsciiPath(Path.TrimEndingDirectorySeparator(parent));
if (asciiParent is null) continue;
var cache = Path.Combine(asciiParent, "QInventor", "RuntimeLinks");
var link = Path.Combine(cache, key);
try
{
Directory.CreateDirectory(cache);
var info = new DirectoryInfo(link);
if (info.Exists)
{
var target = info.ResolveLinkTarget(false)?.FullName;
if (target is null || !string.Equals(Path.TrimEndingDirectorySeparator(Path.GetFullPath(target)),
root, StringComparison.OrdinalIgnoreCase))
throw new IOException("Существующая ссылка CAD-движка указывает на другую папку.");
}
else
{
// Never remove an existing or dangling junction. Only create
// a fresh directory whose exact target is the validated root.
if (info.LinkTarget is not null)
throw new IOException("Служебная ссылка CAD-движка недоступна.");
Directory.CreateDirectory(link);
CreateJunction(link, root);
}
var aliasedEngine = Path.Combine(link, relativeEngine);
if (!File.Exists(aliasedEngine))
throw new IOException("Служебная ссылка CAD-движка не содержит исполняемый файл.");
return aliasedEngine;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or Win32Exception)
{
lastError = ex;
}
}
throw new IOException("Не удалось подготовить служебный путь CAD-движка. Перенесите папку QInventor в путь из латинских символов.", lastError);
}
static void CreateJunction(string link, string target)
{
if (target.StartsWith(@"\\"))
throw new IOException("Сетевая папка без короткого пути не поддерживается CAD-движком.");
var substitute = Encoding.Unicode.GetBytes(@"\??\" + target);
var print = Encoding.Unicode.GetBytes(target);
var buffer = new byte[16 + substitute.Length + 2 + print.Length + 2];
void Write16(int offset, int value) => BitConverter.GetBytes(checked((ushort)value)).CopyTo(buffer, offset);
BitConverter.GetBytes(0xA0000003u).CopyTo(buffer, 0); // IO_REPARSE_TAG_MOUNT_POINT
Write16(4, buffer.Length - 8);
Write16(8, 0);
Write16(10, substitute.Length);
Write16(12, substitute.Length + 2);
Write16(14, print.Length);
substitute.CopyTo(buffer, 16);
print.CopyTo(buffer, 18 + substitute.Length);
using var handle = CreateFileW(link, 0x40000000, 0, IntPtr.Zero, 3,
0x00200000 | 0x02000000, IntPtr.Zero);
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
if (!DeviceIoControl(handle, 0x000900A4, buffer, (uint)buffer.Length,
IntPtr.Zero, 0, out _, IntPtr.Zero))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}