Initial import: renamed to DevDrive

This commit is contained in:
2026-01-05 07:16:16 +11:00
commit 9bf318e41e
10 changed files with 1873 additions and 0 deletions

48
.gitignore vendored Normal file
View File

@@ -0,0 +1,48 @@
# .NET / Visual Studio build outputs
.vs/
[Bb]in/
[Oo]bj/
# VS Code workspace settings
.vscode/
# NuGet packages and artifacts
*.nupkg
packages/
# Test results and coverage
TestResult*/
*.TestResult.xml
coverage/
*.coverage
*.coveragexml
# User-specific and IDE files
*.user
*.suo
*.userprefs
.idea/
*.sln.iml
_ReSharper*/
*.[Rr]e[Ss]harper
# Logs and caches
*.log
*.svclog
*.pidb
*.psess
*.vsp
*.vspx
# Debug symbols
*.pdb
# OS-specific clutter
Thumbs.db
ehthumbs.db
Desktop.ini
.DS_Store
# Publish outputs
publish/
**/PublishOutput/

16
DevDrive/DevDrive.csproj Normal file
View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DokanNet" Version="2.3.0.3" />
<PackageReference Include="System.Management" Version="8.0.0" />
</ItemGroup>
</Project>

174
DevDrive/DriveHider.cs Normal file
View File

@@ -0,0 +1,174 @@
using System.Runtime.Versioning;
using Microsoft.Win32;
namespace DevDrive;
/// <summary>
/// Handles hiding and showing drives from Windows Explorer using registry.
/// </summary>
[SupportedOSPlatform("windows")]
public static class DriveHider
{
private const string LogPrefix = "[DriveHider]";
private const string ExplorerPoliciesKey = @"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer";
private const string NoDrivesValue = "NoDrives";
private static int _hiddenDriveMask;
private static string? _hiddenDriveLetter;
private static int _originalNoDrives;
public static bool HideDrive(string driveLetter)
{
if (_hiddenDriveLetter != null)
{
Console.WriteLine($"{LogPrefix} Drive already hidden");
return true;
}
try
{
var letter = driveLetter.TrimEnd('\\', ':').ToUpperInvariant();
if (letter.Length != 1 || letter[0] < 'A' || letter[0] > 'Z')
{
Console.WriteLine($"{LogPrefix} Invalid drive letter: {driveLetter}");
return false;
}
var driveIndex = letter[0] - 'A';
_hiddenDriveMask = 1 << driveIndex;
_hiddenDriveLetter = letter + ":\\";
using var key = Registry.CurrentUser.CreateSubKey(ExplorerPoliciesKey);
if (key == null)
{
Console.WriteLine($"{LogPrefix} Failed to open registry key");
return false;
}
_originalNoDrives = GetNoDrivesValue(key);
var newValue = _originalNoDrives | _hiddenDriveMask;
key.SetValue(NoDrivesValue, newValue, RegistryValueKind.DWord);
RefreshExplorer();
Console.WriteLine($"{LogPrefix} Hidden drive {letter}: from Explorer");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error hiding drive: {ex.Message}");
_hiddenDriveMask = 0;
_hiddenDriveLetter = null;
return false;
}
}
public static bool ShowDrive()
{
if (_hiddenDriveLetter == null)
{
return true;
}
try
{
using var key = Registry.CurrentUser.OpenSubKey(ExplorerPoliciesKey, writable: true);
if (key == null)
{
Console.WriteLine($"{LogPrefix} Failed to open registry key for restore");
return false;
}
var currentValue = GetNoDrivesValue(key);
var newValue = currentValue & ~_hiddenDriveMask;
if (newValue == 0)
{
key.DeleteValue(NoDrivesValue, throwOnMissingValue: false);
}
else
{
key.SetValue(NoDrivesValue, newValue, RegistryValueKind.DWord);
}
RefreshExplorer();
Console.WriteLine($"{LogPrefix} Restored drive {_hiddenDriveLetter.TrimEnd('\\')} in Explorer");
_hiddenDriveMask = 0;
_hiddenDriveLetter = null;
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error restoring drive: {ex.Message}");
return false;
}
}
public static bool IsHidden => _hiddenDriveLetter != null;
public static string? HiddenDriveLetter => _hiddenDriveLetter;
private static int GetNoDrivesValue(RegistryKey key)
{
var value = key.GetValue(NoDrivesValue);
return value switch
{
null => 0,
int intValue => intValue,
uint uintValue => (int)uintValue,
byte[] bytes when bytes.Length >= 4 => BitConverter.ToInt32(bytes, 0),
byte[] bytes when bytes.Length > 0 => bytes[0],
_ => 0
};
}
private static void RefreshExplorer()
{
try
{
NativeMethods.SendMessageTimeout(
NativeMethods.HWND_BROADCAST,
NativeMethods.WM_SETTINGCHANGE,
IntPtr.Zero,
"Policy",
NativeMethods.SMTO_ABORTIFHUNG,
1000,
out _);
NativeMethods.SHChangeNotify(
NativeMethods.SHCNE_DRIVEADD | NativeMethods.SHCNE_DRIVEREMOVED,
NativeMethods.SHCNF_FLUSHNOWAIT,
IntPtr.Zero,
IntPtr.Zero);
}
catch
{
}
}
private static class NativeMethods
{
public const int SHCNE_DRIVEADD = 0x00000100;
public const int SHCNE_DRIVEREMOVED = 0x00000080;
public const int SHCNF_FLUSHNOWAIT = 0x3000;
public static readonly IntPtr HWND_BROADCAST = new(0xFFFF);
public const int WM_SETTINGCHANGE = 0x001A;
public const int SMTO_ABORTIFHUNG = 0x0002;
[System.Runtime.InteropServices.DllImport("shell32.dll")]
public static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2);
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(
IntPtr hWnd,
int Msg,
IntPtr wParam,
string lParam,
int fuFlags,
int uTimeout,
out IntPtr lpdwResult);
}
}

105
DevDrive/DriveIconHelper.cs Normal file
View File

@@ -0,0 +1,105 @@
using System.Runtime.Versioning;
using Microsoft.Win32;
namespace DevDrive;
/// <summary>
/// Sets custom drive icons via the Windows registry.
/// </summary>
[SupportedOSPlatform("windows")]
public static class DriveIconHelper
{
private const string LogPrefix = "[DriveIcon]";
private const string DriveIconsKey = @"Software\Classes\Applications\Explorer.exe\Drives";
private static string? _currentDriveLetter;
public static bool SetDriveIcon(string driveLetter, string iconPath, string? driveLabel = null)
{
try
{
var letter = driveLetter.TrimEnd('\\', ':').ToUpperInvariant();
if (letter.Length != 1 || letter[0] < 'A' || letter[0] > 'Z')
{
Console.WriteLine($"{LogPrefix} Invalid drive letter: {driveLetter}");
return false;
}
var driveKeyPath = @$"{DriveIconsKey}\{letter}\DefaultIcon";
using var key = Registry.CurrentUser.CreateSubKey(driveKeyPath);
if (key == null)
{
Console.WriteLine($"{LogPrefix} Failed to create registry key");
return false;
}
key.SetValue("", iconPath);
_currentDriveLetter = letter;
if (!string.IsNullOrEmpty(driveLabel))
{
var labelKeyPath = @$"{DriveIconsKey}\{letter}\DefaultLabel";
using var labelKey = Registry.CurrentUser.CreateSubKey(labelKeyPath);
labelKey?.SetValue("", driveLabel);
}
RefreshShell();
Console.WriteLine($"{LogPrefix} Set icon for drive {letter}: to {iconPath}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error setting icon: {ex.Message}");
return false;
}
}
public static void RemoveDriveIcon()
{
if (_currentDriveLetter == null)
{
return;
}
try
{
var driveKeyPath = @$"{DriveIconsKey}\{_currentDriveLetter}";
Registry.CurrentUser.DeleteSubKeyTree(driveKeyPath, throwOnMissingSubKey: false);
Console.WriteLine($"{LogPrefix} Removed icon for drive {_currentDriveLetter}:");
_currentDriveLetter = null;
RefreshShell();
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error removing icon: {ex.Message}");
}
}
private static void RefreshShell()
{
try
{
NativeMethods.SHChangeNotify(
NativeMethods.SHCNE_ASSOCCHANGED,
NativeMethods.SHCNF_IDLIST,
IntPtr.Zero,
IntPtr.Zero);
}
catch
{
}
}
private static class NativeMethods
{
public const int SHCNE_ASSOCCHANGED = 0x08000000;
public const int SHCNF_IDLIST = 0x0000;
[System.Runtime.InteropServices.DllImport("shell32.dll")]
public static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
}

33
DevDrive/FloppyConfig.cs Normal file
View File

@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace DevDrive;
/// <summary>
/// Represents the index.json configuration file structure.
/// </summary>
public class FloppyConfig
{
[JsonPropertyName("driveName")]
public string DriveName { get; set; } = "Dev Disk";
[JsonPropertyName("driveIcon")]
public string DriveIcon { get; set; } = "";
[JsonPropertyName("driveTotalMB")]
public double DriveTotalMB { get; set; } = 100;
[JsonPropertyName("driveFreeMB")]
public double DriveFreeMB { get; set; } = 50;
[JsonPropertyName("paths")]
public List<SymlinkEntry> Paths { get; set; } = [];
}
public class SymlinkEntry
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("path")]
public string Path { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,39 @@
using System.Security.AccessControl;
namespace DevDrive;
/// <summary>
/// Represents a file or directory node in the virtual file system.
/// </summary>
public class FloppyFileNode
{
public string Name { get; set; }
public string FullPath { get; set; }
public bool IsDirectory { get; set; }
public FileAttributes Attributes { get; set; }
public byte[] Data { get; set; }
public DateTime CreationTime { get; set; }
public DateTime LastAccessTime { get; set; }
public DateTime LastWriteTime { get; set; }
public FileSystemSecurity? Security { get; set; }
public string? SymlinkTarget { get; set; }
public bool IsSymlink => !string.IsNullOrEmpty(SymlinkTarget);
public FloppyFileNode(string fullPath, bool isDirectory)
{
FullPath = fullPath.TrimEnd('\\');
Name = Path.GetFileName(fullPath);
if (string.IsNullOrEmpty(Name))
{
Name = "\\";
}
IsDirectory = isDirectory;
Attributes = isDirectory ? FileAttributes.Directory : FileAttributes.Normal;
Data = [];
CreationTime = DateTime.Now;
LastAccessTime = DateTime.Now;
LastWriteTime = DateTime.Now;
}
public long FileSize => IsDirectory ? 0 : Data.Length;
}

View File

@@ -0,0 +1,631 @@
using System.Security.AccessControl;
using DokanNet;
using FileAccess = DokanNet.FileAccess;
namespace DevDrive;
public class FloppyFileSystem : IDokanOperations
{
private const string DefaultVolumeName = "Dev Disk";
private const string FileSystemName = "FAT";
private const uint VolumeSerial = 0x19950101;
private readonly FloppyStorage _storage;
private readonly string _volumeName;
public FloppyFileSystem(string? volumeName = null, long totalBytes = FloppyStorage.DefaultCapacity, long freeBytes = FloppyStorage.DefaultCapacity)
{
_storage = new FloppyStorage(totalBytes, freeBytes);
_volumeName = volumeName ?? DefaultVolumeName;
}
public FloppyFileSystem(Dictionary<string, string>? symlinks, string? volumeName, long totalBytes, long freeBytes)
{
_storage = new FloppyStorage(totalBytes, freeBytes);
_volumeName = volumeName ?? DefaultVolumeName;
if (symlinks != null)
{
foreach (var (path, target) in symlinks)
{
_storage.CreateSymlink(path, target, true);
}
}
}
public NtStatus CreateFile(
string fileName,
FileAccess access,
FileShare share,
FileMode mode,
FileOptions options,
FileAttributes attributes,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
return SymlinkHandler.CreateRealFileOrDirectory(realPath, info.IsDirectory, mode);
}
var node = _storage.GetNode(path);
var parentPath = FloppyStorage.GetParentPath(path);
var parentExists = _storage.Exists(parentPath);
if (info.IsDirectory)
{
switch (mode)
{
case FileMode.CreateNew:
if (node != null)
{
return DokanResult.FileExists;
}
if (!parentExists)
{
return DokanResult.PathNotFound;
}
var newDir = _storage.CreateNode(path, true);
if (newDir == null)
{
return DokanResult.Error;
}
return DokanResult.Success;
case FileMode.Open:
if (node == null)
{
return DokanResult.PathNotFound;
}
if (!node.IsDirectory)
{
return DokanResult.NotADirectory;
}
return DokanResult.Success;
default:
return DokanResult.Success;
}
}
var pathExists = node != null;
var pathIsDirectory = node?.IsDirectory ?? false;
switch (mode)
{
case FileMode.Open:
if (!pathExists)
{
return DokanResult.FileNotFound;
}
if (pathIsDirectory)
{
info.IsDirectory = true;
}
break;
case FileMode.CreateNew:
if (pathExists)
{
return DokanResult.FileExists;
}
if (!parentExists)
{
return DokanResult.PathNotFound;
}
_storage.CreateNode(path, false);
break;
case FileMode.Create:
if (pathExists && pathIsDirectory)
{
return DokanResult.AccessDenied;
}
if (!parentExists)
{
return DokanResult.PathNotFound;
}
if (pathExists)
{
var existing = _storage.GetNode(path);
if (existing != null)
{
existing.Data = [];
existing.LastWriteTime = DateTime.Now;
}
}
else
{
_storage.CreateNode(path, false);
}
break;
case FileMode.OpenOrCreate:
if (!pathExists)
{
if (!parentExists)
{
return DokanResult.PathNotFound;
}
_storage.CreateNode(path, false);
}
else if (pathIsDirectory)
{
info.IsDirectory = true;
}
break;
case FileMode.Truncate:
if (!pathExists)
{
return DokanResult.FileNotFound;
}
if (pathIsDirectory)
{
return DokanResult.AccessDenied;
}
var truncNode = _storage.GetNode(path);
if (truncNode != null)
{
truncNode.Data = [];
truncNode.LastWriteTime = DateTime.Now;
}
break;
case FileMode.Append:
if (!pathExists)
{
if (!parentExists)
{
return DokanResult.PathNotFound;
}
_storage.CreateNode(path, false);
}
break;
}
return DokanResult.Success;
}
public void Cleanup(string fileName, IDokanFileInfo info)
{
if (info.IsDirectory && _storage.Exists(FloppyStorage.NormalizePath(fileName)))
{
return;
}
}
public void CloseFile(string fileName, IDokanFileInfo info)
{
}
public NtStatus ReadFile(
string fileName,
byte[] buffer,
out int bytesRead,
long offset,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
return SymlinkHandler.ReadRealFile(realPath, buffer, out bytesRead, offset);
}
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
bytesRead = 0;
return DokanResult.FileNotFound;
}
node.LastAccessTime = DateTime.Now;
if (offset >= node.Data.Length)
{
bytesRead = 0;
return DokanResult.Success;
}
var toRead = Math.Min(buffer.Length, (int)(node.Data.Length - offset));
Array.Copy(node.Data, offset, buffer, 0, toRead);
bytesRead = toRead;
return DokanResult.Success;
}
public NtStatus WriteFile(
string fileName,
byte[] buffer,
out int bytesWritten,
long offset,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
return SymlinkHandler.WriteRealFile(realPath, buffer, out bytesWritten, offset);
}
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
bytesWritten = 0;
return DokanResult.FileNotFound;
}
if (info.PagingIo)
{
if (offset >= node.Data.Length)
{
bytesWritten = 0;
return DokanResult.Success;
}
var toWrite = Math.Min(buffer.Length, (int)(node.Data.Length - offset));
Array.Copy(buffer, 0, node.Data, offset, toWrite);
bytesWritten = toWrite;
return DokanResult.Success;
}
var newLength = offset + buffer.Length;
var additionalBytes = newLength - node.Data.Length;
if (additionalBytes > 0 && !_storage.HasEnoughSpace(additionalBytes))
{
bytesWritten = 0;
return DokanResult.DiskFull;
}
if (newLength > node.Data.Length)
{
var newData = new byte[newLength];
Array.Copy(node.Data, newData, node.Data.Length);
node.Data = newData;
}
Array.Copy(buffer, 0, node.Data, offset, buffer.Length);
bytesWritten = buffer.Length;
node.LastWriteTime = DateTime.Now;
return DokanResult.Success;
}
public NtStatus FlushFileBuffers(string fileName, IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus GetFileInformation(
string fileName,
out FileInformation fileInfo,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
if (SymlinkHandler.GetRealFileInfo(realPath, out fileInfo))
{
return DokanResult.Success;
}
return DokanResult.FileNotFound;
}
var node = _storage.GetNode(path);
if (node == null)
{
fileInfo = default;
return DokanResult.FileNotFound;
}
fileInfo = new FileInformation
{
FileName = node.Name,
Attributes = node.Attributes,
CreationTime = node.CreationTime,
LastAccessTime = node.LastAccessTime,
LastWriteTime = node.LastWriteTime,
Length = node.FileSize
};
return DokanResult.Success;
}
public NtStatus FindFiles(
string fileName,
out IList<FileInformation> files,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
files = SymlinkHandler.ListRealDirectory(realPath);
return DokanResult.Success;
}
var children = _storage.GetChildren(path);
files = children.Select(node => new FileInformation
{
FileName = node.Name,
Attributes = node.Attributes,
CreationTime = node.CreationTime,
LastAccessTime = node.LastAccessTime,
LastWriteTime = node.LastWriteTime,
Length = node.FileSize
}).ToList();
return DokanResult.Success;
}
public NtStatus FindFilesWithPattern(
string fileName,
string searchPattern,
out IList<FileInformation> files,
IDokanFileInfo info)
{
files = [];
return DokanResult.NotImplemented;
}
public NtStatus SetFileAttributes(
string fileName,
FileAttributes attributes,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
node.Attributes = attributes;
return DokanResult.Success;
}
public NtStatus SetFileTime(
string fileName,
DateTime? creationTime,
DateTime? lastAccessTime,
DateTime? lastWriteTime,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
if (creationTime.HasValue)
{
node.CreationTime = creationTime.Value;
}
if (lastAccessTime.HasValue)
{
node.LastAccessTime = lastAccessTime.Value;
}
if (lastWriteTime.HasValue)
{
node.LastWriteTime = lastWriteTime.Value;
}
return DokanResult.Success;
}
public NtStatus DeleteFile(string fileName, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
if (node.IsDirectory)
{
return DokanResult.AccessDenied;
}
return DokanResult.Success;
}
public NtStatus DeleteDirectory(string fileName, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.PathNotFound;
}
if (!node.IsDirectory)
{
return DokanResult.NotADirectory;
}
var children = _storage.GetChildren(path);
if (children.Any())
{
return DokanResult.DirectoryNotEmpty;
}
return DokanResult.Success;
}
public NtStatus MoveFile(
string oldName,
string newName,
bool replace,
IDokanFileInfo info)
{
var success = _storage.MoveNode(oldName, newName, replace);
return success ? DokanResult.Success : DokanResult.FileNotFound;
}
public NtStatus SetEndOfFile(string fileName, long length, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
return DokanResult.FileNotFound;
}
var additionalBytes = length - node.Data.Length;
if (additionalBytes > 0 && !_storage.HasEnoughSpace(additionalBytes))
{
return DokanResult.DiskFull;
}
if (length != node.Data.Length)
{
var newData = new byte[length];
var copyLength = Math.Min(length, node.Data.Length);
Array.Copy(node.Data, newData, copyLength);
node.Data = newData;
node.LastWriteTime = DateTime.Now;
}
return DokanResult.Success;
}
public NtStatus SetAllocationSize(string fileName, long length, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
return DokanResult.FileNotFound;
}
if (length < node.Data.Length)
{
var newData = new byte[length];
Array.Copy(node.Data, newData, length);
node.Data = newData;
node.LastWriteTime = DateTime.Now;
}
return DokanResult.Success;
}
public NtStatus LockFile(
string fileName,
long offset,
long length,
IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus UnlockFile(
string fileName,
long offset,
long length,
IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus GetDiskFreeSpace(
out long freeBytesAvailable,
out long totalNumberOfBytes,
out long totalNumberOfFreeBytes,
IDokanFileInfo info)
{
freeBytesAvailable = _storage.FreeSpace;
totalNumberOfBytes = _storage.Capacity;
totalNumberOfFreeBytes = _storage.FreeSpace;
return DokanResult.Success;
}
public NtStatus GetVolumeInformation(
out string volumeLabel,
out FileSystemFeatures features,
out string fileSystemName,
out uint maximumComponentLength,
IDokanFileInfo info)
{
volumeLabel = _volumeName;
fileSystemName = FileSystemName;
maximumComponentLength = 255;
features = FileSystemFeatures.CasePreservedNames
| FileSystemFeatures.UnicodeOnDisk;
return DokanResult.Success;
}
public NtStatus GetFileSecurity(
string fileName,
out FileSystemSecurity? security,
AccessControlSections sections,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
security = null;
return DokanResult.FileNotFound;
}
security = node.Security;
return DokanResult.Success;
}
public NtStatus SetFileSecurity(
string fileName,
FileSystemSecurity security,
AccessControlSections sections,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
node.Security = security;
return DokanResult.Success;
}
public NtStatus Mounted(string mountPoint, IDokanFileInfo info)
{
Console.WriteLine($"Drive mounted at {mountPoint}");
return DokanResult.Success;
}
public NtStatus Unmounted(IDokanFileInfo info)
{
Console.WriteLine("Drive unmounted");
return DokanResult.Success;
}
public NtStatus FindStreams(
string fileName,
out IList<FileInformation> streams,
IDokanFileInfo info)
{
streams = [];
return DokanResult.NotImplemented;
}
}

213
DevDrive/FloppyStorage.cs Normal file
View File

@@ -0,0 +1,213 @@
using System.Collections.Concurrent;
namespace DevDrive;
/// <summary>
/// Manages the in-memory file system storage for the virtual drive.
/// </summary>
public class FloppyStorage
{
public const long DefaultCapacity = 1_474_560;
public const int SectorSize = 512;
public const int AllocationUnitSize = 512;
private readonly ConcurrentDictionary<string, FloppyFileNode> _files = new(StringComparer.OrdinalIgnoreCase);
private readonly object _lock = new();
public long Capacity { get; }
public long FreeSpace { get; }
public FloppyStorage() : this(DefaultCapacity, DefaultCapacity)
{
}
public FloppyStorage(long totalBytes, long freeBytes)
{
Capacity = totalBytes;
FreeSpace = freeBytes;
var root = new FloppyFileNode("\\", true);
_files["\\"] = root;
}
public FloppyFileNode? CreateSymlink(string path, string targetPath, bool isDirectory)
{
path = NormalizePath(path);
if (_files.ContainsKey(path))
{
return null;
}
var parentPath = GetParentPath(path);
if (!_files.ContainsKey(parentPath))
{
return null;
}
var node = new FloppyFileNode(path, isDirectory)
{
SymlinkTarget = targetPath,
Attributes = (isDirectory ? FileAttributes.Directory : FileAttributes.Normal) | FileAttributes.ReparsePoint
};
_files[path] = node;
return node;
}
public static string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
{
return "\\";
}
path = path.Replace('/', '\\');
if (!path.StartsWith('\\'))
{
path = "\\" + path;
}
while (path.Contains("\\\\"))
{
path = path.Replace("\\\\", "\\");
}
if (path.Length > 1 && path.EndsWith('\\'))
{
path = path.TrimEnd('\\');
}
return path;
}
public static string GetParentPath(string path)
{
path = NormalizePath(path);
if (path == "\\")
{
return "\\";
}
var lastSlash = path.LastIndexOf('\\');
return lastSlash <= 0 ? "\\" : path[..lastSlash];
}
public bool Exists(string path)
{
path = NormalizePath(path);
return _files.ContainsKey(path);
}
public FloppyFileNode? GetNode(string path)
{
path = NormalizePath(path);
_files.TryGetValue(path, out var node);
return node;
}
public FloppyFileNode? CreateNode(string path, bool isDirectory)
{
path = NormalizePath(path);
if (_files.ContainsKey(path))
{
return _files[path];
}
var parentPath = GetParentPath(path);
if (!_files.ContainsKey(parentPath))
{
return null;
}
var node = new FloppyFileNode(path, isDirectory);
_files[path] = node;
return node;
}
public bool DeleteNode(string path)
{
path = NormalizePath(path);
if (path == "\\")
{
return false;
}
var node = GetNode(path);
if (node == null)
{
return false;
}
if (node.IsDirectory)
{
var children = GetChildren(path);
if (children.Any())
{
return false;
}
}
return _files.TryRemove(path, out _);
}
public IEnumerable<FloppyFileNode> GetChildren(string path)
{
path = NormalizePath(path);
foreach (var kvp in _files)
{
var filePath = kvp.Key;
if (filePath == path)
{
continue;
}
var parentPath = GetParentPath(filePath);
if (string.Equals(parentPath, path, StringComparison.OrdinalIgnoreCase))
{
yield return kvp.Value;
}
}
}
public bool MoveNode(string oldPath, string newPath, bool replace)
{
oldPath = NormalizePath(oldPath);
newPath = NormalizePath(newPath);
if (!_files.TryGetValue(oldPath, out var node))
{
return false;
}
if (_files.ContainsKey(newPath))
{
if (!replace)
{
return false;
}
_files.TryRemove(newPath, out _);
}
var newParentPath = GetParentPath(newPath);
if (!_files.ContainsKey(newParentPath))
{
return false;
}
_files.TryRemove(oldPath, out _);
node.FullPath = newPath;
node.Name = Path.GetFileName(newPath);
node.LastWriteTime = DateTime.Now;
_files[newPath] = node;
return true;
}
public bool HasEnoughSpace(long additionalBytes)
{
return FreeSpace >= additionalBytes;
}
}

413
DevDrive/Program.cs Normal file
View File

@@ -0,0 +1,413 @@
using System.Management;
using System.Runtime.Versioning;
using System.Text.Json;
using DokanNet;
using DokanNet.Logging;
namespace DevDrive;
/// <summary>
/// Entry point for the virtual drive application.
/// Monitors a config drive via WMI events and creates symlinks based on index.json.
/// </summary>
[SupportedOSPlatform("windows")]
public static class Program
{
private const string DefaultMountPoint = "A:\\";
private const string DefaultConfigDrive = "C:\\PhysicalDrive\\";
private const string ConfigFile = "index.json";
private const string LogPrefix = "[DevDrive]";
private static Dokan? _dokan;
private static DokanInstance? _dokanInstance;
private static readonly ManualResetEvent ShutdownEvent = new(false);
private static bool _isMounted;
private static string _configDrive = DefaultConfigDrive;
private static string _configFilePath = "";
private static string _mountPoint = DefaultMountPoint;
private static FloppyConfig? _lastConfig;
private static readonly object MountLock = new();
private static bool _isConfigDriveLetter;
/// <summary>
/// Main entry point.
/// </summary>
/// <param name="args">Command line arguments. [0] = mount point, [1] = config drive.</param>
public static void Main(string[] args)
{
_mountPoint = args.Length > 0 ? args[0] : DefaultMountPoint;
_configDrive = args.Length > 1 ? args[1] : DefaultConfigDrive;
if (!_mountPoint.EndsWith("\\"))
{
_mountPoint += "\\";
}
if (!_configDrive.EndsWith("\\"))
{
_configDrive += "\\";
}
_configFilePath = Path.Combine(_configDrive, ConfigFile);
_isConfigDriveLetter = _configDrive.Length <= 3 && _configDrive[1] == ':';
Console.WriteLine($"{LogPrefix} DevDrive");
Console.WriteLine($"{LogPrefix} ==================");
Console.WriteLine($"{LogPrefix} Watching for config drive: {_configDrive}");
Console.WriteLine($"{LogPrefix} Config file: {_configFilePath}");
Console.WriteLine($"{LogPrefix} Mount point: {_mountPoint}");
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit.");
Console.WriteLine();
Console.CancelKeyPress += (sender, eventArgs) =>
{
eventArgs.Cancel = true;
Console.WriteLine($"{LogPrefix} Shutting down...");
ShutdownEvent.Set();
};
try
{
_dokan = new Dokan(new ConsoleLogger(LogPrefix));
if (_isConfigDriveLetter)
{
StartWmiWatchers();
}
else
{
StartFileSystemWatcher();
}
CheckAndMountIfReady();
ShutdownEvent.WaitOne();
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Fatal error: {ex.Message}");
Environment.Exit(1);
}
finally
{
UnmountFloppy();
_dokan?.Dispose();
}
}
private static void StartFileSystemWatcher()
{
StartVolumeWatchers();
Console.WriteLine($"{LogPrefix} Volume watchers started for {_configDrive}");
Console.WriteLine($"{LogPrefix} Watching for: {ConfigFile}");
}
private static void StartVolumeWatchers()
{
try
{
var insertQuery = new WqlEventQuery(
"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Volume'"
);
var insertWatcher = new ManagementEventWatcher(insertQuery);
insertWatcher.EventArrived += (s, e) =>
{
Console.WriteLine($"{LogPrefix} Volume added, checking config...");
Thread.Sleep(1000);
CheckAndMountIfReady();
};
insertWatcher.Start();
var removeQuery = new WqlEventQuery(
"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Volume'"
);
var removeWatcher = new ManagementEventWatcher(removeQuery);
removeWatcher.EventArrived += (s, e) =>
{
Console.WriteLine($"{LogPrefix} Volume removed");
lock (MountLock)
{
if (_isMounted && !IsConfigAccessible())
{
Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting...");
UnmountFloppy();
_lastConfig = null;
}
}
};
removeWatcher.Start();
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Warning: Could not start WMI volume watchers: {ex.Message}");
}
}
private static bool IsConfigAccessible()
{
try
{
if (!Directory.Exists(_configDrive))
{
return false;
}
var _ = Directory.GetFiles(_configDrive);
return File.Exists(_configFilePath);
}
catch
{
return false;
}
}
private static void StartWmiWatchers()
{
var driveLetter = _configDrive.TrimEnd('\\');
var insertQuery = new WqlEventQuery(
"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var insertWatcher = new ManagementEventWatcher(insertQuery);
insertWatcher.EventArrived += OnDriveInserted;
insertWatcher.Start();
var removeQuery = new WqlEventQuery(
"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var removeWatcher = new ManagementEventWatcher(removeQuery);
removeWatcher.EventArrived += OnDriveRemoved;
removeWatcher.Start();
Console.WriteLine($"{LogPrefix} WMI watchers started (event-based, no polling)");
Console.WriteLine($"{LogPrefix} Watching for drive: {driveLetter}");
}
private static void OnDriveInserted(object sender, EventArrivedEventArgs e)
{
try
{
var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
var deviceId = targetInstance["DeviceID"]?.ToString();
if (string.IsNullOrEmpty(deviceId))
{
return;
}
var insertedDrive = deviceId + "\\";
if (string.Equals(insertedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!");
Thread.Sleep(500);
CheckAndMountIfReady();
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error handling drive insert: {ex.Message}");
}
}
private static void OnDriveRemoved(object sender, EventArrivedEventArgs e)
{
try
{
var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
var deviceId = targetInstance["DeviceID"]?.ToString();
if (string.IsNullOrEmpty(deviceId))
{
return;
}
var removedDrive = deviceId + "\\";
if (string.Equals(removedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!");
lock (MountLock)
{
if (_isMounted)
{
Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting...");
UnmountFloppy();
_lastConfig = null;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error handling drive removal: {ex.Message}");
}
}
private static void CheckAndMountIfReady()
{
lock (MountLock)
{
var driveExists = Directory.Exists(_configDrive);
var configExists = driveExists && File.Exists(_configFilePath);
if (!driveExists)
{
Console.WriteLine($"{LogPrefix} Waiting for {_configDrive} to be connected...");
return;
}
if (!configExists)
{
Console.WriteLine($"{LogPrefix} Drive present but {ConfigFile} not found");
return;
}
var config = ReadConfig(_configFilePath);
if (config == null || config.Paths.Count == 0)
{
Console.WriteLine($"{LogPrefix} Config empty or invalid");
if (_isMounted)
{
UnmountFloppy();
_lastConfig = null;
}
return;
}
var configChanged = !ConfigEquals(_lastConfig, config);
if (!_isMounted || configChanged)
{
if (_isMounted && configChanged)
{
Console.WriteLine($"{LogPrefix} Config changed, remounting...");
UnmountFloppy();
}
MountFloppy(config);
_lastConfig = config;
}
}
}
private static FloppyConfig? ReadConfig(string configFilePath)
{
try
{
var json = File.ReadAllText(configFilePath);
return JsonSerializer.Deserialize<FloppyConfig>(json);
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error reading config: {ex.Message}");
return null;
}
}
private static bool ConfigEquals(FloppyConfig? a, FloppyConfig? b)
{
if (a == null || b == null)
{
return a == b;
}
if (a.Paths.Count != b.Paths.Count)
{
return false;
}
for (var i = 0; i < a.Paths.Count; i++)
{
if (a.Paths[i].Name != b.Paths[i].Name || a.Paths[i].Path != b.Paths[i].Path)
{
return false;
}
}
return true;
}
private static void MountFloppy(FloppyConfig config)
{
if (_dokan == null)
{
return;
}
try
{
var symlinks = new Dictionary<string, string>();
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Creating symlinks:");
foreach (var entry in config.Paths)
{
var symlinkPath = "\\" + entry.Name;
symlinks[symlinkPath] = entry.Path;
Console.WriteLine($"{LogPrefix} {entry.Name} -> {entry.Path}");
}
Console.WriteLine();
var totalBytes = (long)(config.DriveTotalMB * 1024 * 1024);
var freeBytes = (long)(config.DriveFreeMB * 1024 * 1024);
Console.WriteLine($"{LogPrefix} Drive size: {config.DriveTotalMB} MB total, {config.DriveFreeMB} MB free");
var floppyFs = new FloppyFileSystem(symlinks, config.DriveName, totalBytes, freeBytes);
var dokanBuilder = new DokanInstanceBuilder(_dokan)
.ConfigureOptions(options =>
{
options.Options = DokanOptions.MountManager | DokanOptions.CurrentSession;
options.MountPoint = _mountPoint;
options.SingleThread = false;
options.AllocationUnitSize = FloppyStorage.AllocationUnitSize;
options.SectorSize = FloppyStorage.SectorSize;
});
_dokanInstance = dokanBuilder.Build(floppyFs);
_isMounted = true;
Console.WriteLine($"{LogPrefix} Mounted at {_mountPoint}");
var iconPath = string.IsNullOrWhiteSpace(config.DriveIcon)
? "%SystemRoot%\\System32\\shell32.dll,6"
: config.DriveIcon;
DriveIconHelper.SetDriveIcon(_mountPoint, iconPath, config.DriveName);
}
catch (DokanException ex)
{
Console.WriteLine($"{LogPrefix} Mount error: {ex.Message}");
_isMounted = false;
}
}
private static void UnmountFloppy()
{
if (!_isMounted)
{
return;
}
try
{
DriveIconHelper.RemoveDriveIcon();
_dokan?.RemoveMountPoint(_mountPoint);
_dokanInstance?.Dispose();
_dokanInstance = null;
_isMounted = false;
Console.WriteLine($"{LogPrefix} Unmounted");
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Unmount error: {ex.Message}");
}
}
}

201
DevDrive/SymlinkHandler.cs Normal file
View File

@@ -0,0 +1,201 @@
using DokanNet;
namespace DevDrive;
/// <summary>
/// Handles passthrough operations for symlinks to real file system paths.
/// </summary>
public static class SymlinkHandler
{
public static bool TryResolveSymlink(string virtualPath, FloppyStorage storage, out string? realPath)
{
realPath = null;
virtualPath = FloppyStorage.NormalizePath(virtualPath);
var node = storage.GetNode(virtualPath);
if (node?.IsSymlink == true)
{
realPath = node.SymlinkTarget;
return true;
}
var parts = virtualPath.Split('\\', StringSplitOptions.RemoveEmptyEntries);
var currentPath = "\\";
for (var i = 0; i < parts.Length; i++)
{
currentPath = currentPath == "\\" ? "\\" + parts[i] : currentPath + "\\" + parts[i];
var currentNode = storage.GetNode(currentPath);
if (currentNode?.IsSymlink == true)
{
var remainingPath = string.Join("\\", parts.Skip(i + 1));
realPath = string.IsNullOrEmpty(remainingPath)
? currentNode.SymlinkTarget
: Path.Combine(currentNode.SymlinkTarget!, remainingPath);
return true;
}
}
return false;
}
public static IList<FileInformation> ListRealDirectory(string realPath)
{
var files = new List<FileInformation>();
try
{
if (!Directory.Exists(realPath))
{
return files;
}
foreach (var entry in new DirectoryInfo(realPath).EnumerateFileSystemInfos())
{
files.Add(new FileInformation
{
FileName = entry.Name,
Attributes = entry.Attributes,
CreationTime = entry.CreationTime,
LastAccessTime = entry.LastAccessTime,
LastWriteTime = entry.LastWriteTime,
Length = entry is FileInfo fi ? fi.Length : 0
});
}
}
catch
{
}
return files;
}
public static bool GetRealFileInfo(string realPath, out FileInformation fileInfo)
{
fileInfo = default;
try
{
if (File.Exists(realPath))
{
var fi = new FileInfo(realPath);
fileInfo = new FileInformation
{
FileName = fi.Name,
Attributes = fi.Attributes,
CreationTime = fi.CreationTime,
LastAccessTime = fi.LastAccessTime,
LastWriteTime = fi.LastWriteTime,
Length = fi.Length
};
return true;
}
if (Directory.Exists(realPath))
{
var di = new DirectoryInfo(realPath);
fileInfo = new FileInformation
{
FileName = di.Name,
Attributes = di.Attributes,
CreationTime = di.CreationTime,
LastAccessTime = di.LastAccessTime,
LastWriteTime = di.LastWriteTime,
Length = 0
};
return true;
}
}
catch
{
}
return false;
}
public static NtStatus ReadRealFile(string realPath, byte[] buffer, out int bytesRead, long offset)
{
bytesRead = 0;
try
{
using var stream = new FileStream(realPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite);
stream.Seek(offset, SeekOrigin.Begin);
bytesRead = stream.Read(buffer, 0, buffer.Length);
return DokanResult.Success;
}
catch (FileNotFoundException)
{
return DokanResult.FileNotFound;
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
catch
{
return DokanResult.Error;
}
}
public static NtStatus WriteRealFile(string realPath, byte[] buffer, out int bytesWritten, long offset)
{
bytesWritten = 0;
try
{
using var stream = new FileStream(realPath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write, FileShare.ReadWrite);
stream.Seek(offset, SeekOrigin.Begin);
stream.Write(buffer, 0, buffer.Length);
bytesWritten = buffer.Length;
return DokanResult.Success;
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
catch
{
return DokanResult.Error;
}
}
public static NtStatus CreateRealFileOrDirectory(string realPath, bool isDirectory, FileMode mode)
{
try
{
if (isDirectory)
{
if (mode == FileMode.CreateNew)
{
Directory.CreateDirectory(realPath);
}
return DokanResult.Success;
}
switch (mode)
{
case FileMode.CreateNew:
using (File.Create(realPath)) { }
break;
case FileMode.Create:
case FileMode.OpenOrCreate:
using (new FileStream(realPath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite)) { }
break;
default:
break;
}
return DokanResult.Success;
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
catch
{
return DokanResult.Error;
}
}
}