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()); } }