mirror of
https://github.com/zhicheng233/VHDPatchFix.git
synced 2026-06-16 17:09:49 -04:00
frist commit
This commit is contained in:
34
AGENTS.md
Normal file
34
AGENTS.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
This repository contains a small .NET CLI for patching the parent path stored in classic VHD differencing disks.
|
||||||
|
|
||||||
|
- `src/VHDPatchFix/` contains the CLI, VHD footer/header parsing, checksum logic, and patch operation.
|
||||||
|
- `VHDPatchFix.sln` is the canonical solution file for build and test commands.
|
||||||
|
|
||||||
|
Keep VHD binary parsing code isolated in small classes. Prefer adding focused helpers under `src/VHDPatchFix/` instead of expanding `Program.cs`.
|
||||||
|
|
||||||
|
## Build, and Development Commands
|
||||||
|
|
||||||
|
- `dotnet build VHDPatchFix.sln -v minimal` restores and builds the CLI and tests.
|
||||||
|
- `dotnet run --project src\VHDPatchFix\VHDPatchFix.csproj -- --help` shows CLI usage.
|
||||||
|
- `dotnet run --project src\VHDPatchFix\VHDPatchFix.csproj -- --vhd "I:\internal_1.vhd" --parent "\Device\FscryptDisk_APP_0\internal_0.vhd" --dry-run` validates a target without writing.
|
||||||
|
|
||||||
|
Use `--dry-run` before patching real disks. Ensure the child VHD is detached before writing.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
|
Use C# nullable reference types and implicit usings as configured in the project files. Use four-space indentation, PascalCase for public types and methods, and camelCase for locals and parameters.
|
||||||
|
|
||||||
|
Keep binary offsets explicit and close to the relevant VHD structure parser. Avoid unrelated formatting-only edits in functional changes.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
This checkout has no Git history, so no repository-specific convention is established. Use short imperative subjects such as `Add VHD parent locator patcher`.
|
||||||
|
|
||||||
|
Pull requests should include the target scenario, before/after command output when relevant, and test results from `dotnet build` plus the test runner.
|
||||||
|
|
||||||
|
## Security & Data Safety
|
||||||
|
|
||||||
|
Do not patch attached VHDs. Keep backup creation enabled by default for real disks. Avoid logging full local paths unless needed for troubleshooting, and never validate parent existence as a requirement because NT device paths may only exist on another machine.
|
||||||
37
README.md
Normal file
37
README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# VHDPatchFix
|
||||||
|
|
||||||
|
`vhdfix` patches the parent path stored inside a classic VHD differencing disk. It is intended for cases where the parent VHD was moved and PowerShell `Set-VHD` cannot update the stored parent locator.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Preview a change without writing:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src\VHDPatchFix\VHDPatchFix.csproj -- --vhd "I:\B.vhd" --parent "I:\A.vhd" --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Patch the VHD and create the default `.bak` backup:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project src\VHDPatchFix\VHDPatchFix.csproj -- --vhd "I:\B.vhd" --parent "I:\A.vhd"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify after writing:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-VHD -Path "I:\A.vhd"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Notes
|
||||||
|
|
||||||
|
- Only classic `.vhd` differencing disks are supported.
|
||||||
|
- `.vhdx` is not supported.
|
||||||
|
- The child VHD should be detached before patching.
|
||||||
|
- The tool does not require the new parent path to exist on the current computer.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build VHDPatchFix.sln -v minimal
|
||||||
|
dotnet run --project src\VHDPatchFix\VHDPatchFix.csproj -- --help
|
||||||
|
```
|
||||||
22
VHDPatchFix.sln
Normal file
22
VHDPatchFix.sln
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VHDPatchFix", "src\VHDPatchFix\VHDPatchFix.csproj", "{A6DA9E21-3118-4CC2-9E96-6C623F4E3C7E}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A6DA9E21-3118-4CC2-9E96-6C623F4E3C7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A6DA9E21-3118-4CC2-9E96-6C623F4E3C7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A6DA9E21-3118-4CC2-9E96-6C623F4E3C7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A6DA9E21-3118-4CC2-9E96-6C623F4E3C7E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2A3A1C8D-4613-4C43-9D73-EB6965E375E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2A3A1C8D-4613-4C43-9D73-EB6965E375E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2A3A1C8D-4613-4C43-9D73-EB6965E375E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2A3A1C8D-4613-4C43-9D73-EB6965E375E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
15
src/VHDPatchFix/.idea/.idea.VHDPatchFix.dir/.idea/.gitignore
generated
vendored
Normal file
15
src/VHDPatchFix/.idea/.idea.VHDPatchFix.dir/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Rider 忽略的文件
|
||||||
|
/contentModel.xml
|
||||||
|
/.idea.VHDPatchFix.iml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
/modules.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
90
src/VHDPatchFix/DynamicHeader.cs
Normal file
90
src/VHDPatchFix/DynamicHeader.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public sealed class DynamicHeader
|
||||||
|
{
|
||||||
|
public DynamicHeader(long offset, byte[] bytes)
|
||||||
|
{
|
||||||
|
Offset = offset;
|
||||||
|
Bytes = bytes;
|
||||||
|
ParentLocators = Enumerable.Range(0, 8).Select(index => ParentLocator.Read(bytes, index)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Offset { get; }
|
||||||
|
public byte[] Bytes { get; }
|
||||||
|
public ParentLocator[] ParentLocators { get; private set; }
|
||||||
|
|
||||||
|
public string ParentUnicodeName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
byte[] nameBytes = Bytes.AsSpan(64, 512).ToArray();
|
||||||
|
string value = Encoding.BigEndianUnicode.GetString(nameBytes);
|
||||||
|
return value.TrimEnd('\0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DynamicHeader Read(Stream stream, ulong offset)
|
||||||
|
{
|
||||||
|
if (offset > long.MaxValue || offset + VhdConstants.DynamicHeaderSize > (ulong)stream.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("The VHD dynamic header offset is outside the file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = new byte[VhdConstants.DynamicHeaderSize];
|
||||||
|
stream.Position = (long)offset;
|
||||||
|
ReadExactly(stream, bytes);
|
||||||
|
|
||||||
|
string cookie = Encoding.ASCII.GetString(bytes, 0, 8);
|
||||||
|
if (cookie != VhdConstants.DynamicHeaderCookie)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("The VHD dynamic header cookie is invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint storedChecksum = Endian.ReadUInt32(bytes, 36);
|
||||||
|
Endian.WriteUInt32(bytes, 36, 0);
|
||||||
|
uint computedChecksum = VhdChecksum.Compute(bytes);
|
||||||
|
Endian.WriteUInt32(bytes, 36, storedChecksum);
|
||||||
|
if (storedChecksum != computedChecksum)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("The VHD dynamic header checksum is invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DynamicHeader((long)offset, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParentUnicodeName(string parentPath)
|
||||||
|
{
|
||||||
|
Bytes.AsSpan(64, 512).Clear();
|
||||||
|
string value = parentPath.Length > 255 ? parentPath[..255] : parentPath;
|
||||||
|
Encoding.BigEndianUnicode.GetBytes(value, Bytes.AsSpan(64, 512));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLocator(ParentLocator locator)
|
||||||
|
{
|
||||||
|
locator.Write(Bytes);
|
||||||
|
ParentLocators[locator.Index] = locator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateChecksum()
|
||||||
|
{
|
||||||
|
Endian.WriteUInt32(Bytes, 36, 0);
|
||||||
|
Endian.WriteUInt32(Bytes, 36, VhdChecksum.Compute(Bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReadExactly(Stream stream, byte[] buffer)
|
||||||
|
{
|
||||||
|
int read = 0;
|
||||||
|
while (read < buffer.Length)
|
||||||
|
{
|
||||||
|
int count = stream.Read(buffer, read, buffer.Length - read);
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
|
||||||
|
read += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/VHDPatchFix/Endian.cs
Normal file
26
src/VHDPatchFix/Endian.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public static class Endian
|
||||||
|
{
|
||||||
|
public static uint ReadUInt32(ReadOnlySpan<byte> bytes, int offset)
|
||||||
|
{
|
||||||
|
return BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(offset, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ulong ReadUInt64(ReadOnlySpan<byte> bytes, int offset)
|
||||||
|
{
|
||||||
|
return BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(offset, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteUInt32(Span<byte> bytes, int offset, uint value)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(bytes.Slice(offset, 4), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteUInt64(Span<byte> bytes, int offset, ulong value)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt64BigEndian(bytes.Slice(offset, 8), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/VHDPatchFix/ParentLocator.cs
Normal file
38
src/VHDPatchFix/ParentLocator.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public sealed record ParentLocator(int Index, string PlatformCode, uint PlatformDataSpace, uint PlatformDataLength, ulong PlatformDataOffset)
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> WindowsCodes = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
"W2ku",
|
||||||
|
"W2ru"
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool IsPresent => PlatformCode.Trim('\0') != string.Empty && PlatformDataSpace > 0 && PlatformDataOffset > 0;
|
||||||
|
|
||||||
|
public bool IsWindowsUnicode => WindowsCodes.Contains(PlatformCode);
|
||||||
|
|
||||||
|
public static ParentLocator Read(ReadOnlySpan<byte> header, int index)
|
||||||
|
{
|
||||||
|
int offset = 576 + index * 24;
|
||||||
|
string platformCode = Encoding.ASCII.GetString(header.Slice(offset, 4));
|
||||||
|
return new ParentLocator(
|
||||||
|
index,
|
||||||
|
platformCode,
|
||||||
|
Endian.ReadUInt32(header, offset + 4),
|
||||||
|
Endian.ReadUInt32(header, offset + 8),
|
||||||
|
Endian.ReadUInt64(header, offset + 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(Span<byte> header)
|
||||||
|
{
|
||||||
|
int offset = 576 + Index * 24;
|
||||||
|
Encoding.ASCII.GetBytes(PlatformCode, header.Slice(offset, 4));
|
||||||
|
Endian.WriteUInt32(header, offset + 4, PlatformDataSpace);
|
||||||
|
Endian.WriteUInt32(header, offset + 8, PlatformDataLength);
|
||||||
|
Endian.WriteUInt32(header, offset + 12, 0);
|
||||||
|
Endian.WriteUInt64(header, offset + 16, PlatformDataOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/VHDPatchFix/PatchOptions.cs
Normal file
9
src/VHDPatchFix/PatchOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public sealed record PatchOptions(
|
||||||
|
string VhdPath,
|
||||||
|
string ParentPath,
|
||||||
|
bool DryRun,
|
||||||
|
bool NoBackup,
|
||||||
|
string? BackupPath,
|
||||||
|
bool Verbose);
|
||||||
10
src/VHDPatchFix/PatchResult.cs
Normal file
10
src/VHDPatchFix/PatchResult.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public sealed record PatchResult(
|
||||||
|
string VhdPath,
|
||||||
|
string NewParentPath,
|
||||||
|
string? OldParentPath,
|
||||||
|
string? BackupPath,
|
||||||
|
bool DryRun,
|
||||||
|
bool UsedRelocation,
|
||||||
|
int UpdatedLocatorCount);
|
||||||
169
src/VHDPatchFix/Program.cs
Normal file
169
src/VHDPatchFix/Program.cs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using VHDPatchFix;
|
||||||
|
|
||||||
|
return ProgramMain.Run(args);
|
||||||
|
|
||||||
|
public static class ProgramMain
|
||||||
|
{
|
||||||
|
public static int Run(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (args.Length == 0 || args.Contains("--help") || args.Contains("-h"))
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return args.Length == 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOptions options = ParseArgs(args);
|
||||||
|
PatchResult result = VhdParentPathPatcher.Patch(options);
|
||||||
|
PrintResult(result);
|
||||||
|
|
||||||
|
if (options.Verbose && !options.DryRun)
|
||||||
|
{
|
||||||
|
TryRunGetVhd(options.VhdPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PatchOptions ParseArgs(string[] args)
|
||||||
|
{
|
||||||
|
string? vhdPath = null;
|
||||||
|
string? parentPath = null;
|
||||||
|
string? backupPath = null;
|
||||||
|
bool dryRun = false;
|
||||||
|
bool noBackup = false;
|
||||||
|
bool verbose = false;
|
||||||
|
|
||||||
|
for (int index = 0; index < args.Length; index++)
|
||||||
|
{
|
||||||
|
string arg = args[index];
|
||||||
|
switch (arg)
|
||||||
|
{
|
||||||
|
case "--vhd":
|
||||||
|
vhdPath = ReadValue(args, ref index, arg);
|
||||||
|
break;
|
||||||
|
case "--parent":
|
||||||
|
parentPath = ReadValue(args, ref index, arg);
|
||||||
|
break;
|
||||||
|
case "--backup-path":
|
||||||
|
backupPath = ReadValue(args, ref index, arg);
|
||||||
|
break;
|
||||||
|
case "--dry-run":
|
||||||
|
dryRun = true;
|
||||||
|
break;
|
||||||
|
case "--no-backup":
|
||||||
|
noBackup = true;
|
||||||
|
break;
|
||||||
|
case "--verbose":
|
||||||
|
verbose = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException($"Unknown argument: {arg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(vhdPath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Missing required argument: --vhd <path>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(parentPath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Missing required argument: --parent <path>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noBackup && backupPath is not null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("--no-backup cannot be used with --backup-path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PatchOptions(vhdPath, parentPath, dryRun, noBackup, backupPath, verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadValue(string[] args, ref int index, string name)
|
||||||
|
{
|
||||||
|
if (index + 1 >= args.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Missing value for {name}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
return args[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintUsage()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Usage:");
|
||||||
|
Console.WriteLine(" vhdfix --vhd <child.vhd> --parent <new-parent-path> [--dry-run] [--no-backup] [--backup-path <path>] [--verbose]");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Example:");
|
||||||
|
Console.WriteLine(@" vhdfix --vhd ""I:\internal_1.vhd"" --parent ""\Device\FscryptDisk_APP_0\internal_0.vhd""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintResult(PatchResult result)
|
||||||
|
{
|
||||||
|
Console.WriteLine(result.DryRun ? "Dry run completed." : "VHD parent path updated.");
|
||||||
|
Console.WriteLine($"VHD: {result.VhdPath}");
|
||||||
|
Console.WriteLine($"Old parent: {result.OldParentPath ?? "(not readable)"}");
|
||||||
|
Console.WriteLine($"New parent: {result.NewParentPath}");
|
||||||
|
Console.WriteLine($"Updated locators: {result.UpdatedLocatorCount}");
|
||||||
|
Console.WriteLine($"Relocation needed: {result.UsedRelocation}");
|
||||||
|
|
||||||
|
if (!result.DryRun)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(result.BackupPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Backup: {result.BackupPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($@"Verify with: Get-VHD -Path ""{result.VhdPath}""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryRunGetVhd(string vhdPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessStartInfo startInfo = new()
|
||||||
|
{
|
||||||
|
FileName = "powershell.exe",
|
||||||
|
ArgumentList = { "-NoProfile", "-Command", $"Get-VHD -Path '{vhdPath.Replace("'", "''")}' | Select-Object -ExpandProperty ParentPath" },
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using Process? process = Process.Start(startInfo);
|
||||||
|
if (process is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||||
|
string error = process.StandardError.ReadToEnd().Trim();
|
||||||
|
process.WaitForExit(5000);
|
||||||
|
|
||||||
|
if (process.ExitCode == 0 && output.Length > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Get-VHD ParentPath: {output}");
|
||||||
|
}
|
||||||
|
else if (error.Length > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Get-VHD verification skipped: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.WriteLine("Get-VHD verification skipped: PowerShell or Hyper-V cmdlets are unavailable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/VHDPatchFix/VHDPatchFix.csproj
Normal file
9
src/VHDPatchFix/VHDPatchFix.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>vhdfix</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
15
src/VHDPatchFix/VhdChecksum.cs
Normal file
15
src/VHDPatchFix/VhdChecksum.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public static class VhdChecksum
|
||||||
|
{
|
||||||
|
public static uint Compute(ReadOnlySpan<byte> bytes)
|
||||||
|
{
|
||||||
|
uint sum = 0;
|
||||||
|
foreach (byte value in bytes)
|
||||||
|
{
|
||||||
|
sum += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ~sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/VHDPatchFix/VhdConstants.cs
Normal file
11
src/VHDPatchFix/VhdConstants.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public static class VhdConstants
|
||||||
|
{
|
||||||
|
public const int SectorSize = 512;
|
||||||
|
public const int FooterSize = 512;
|
||||||
|
public const int DynamicHeaderSize = 1024;
|
||||||
|
public const uint DifferencingDiskType = 4;
|
||||||
|
public const string FooterCookie = "conectix";
|
||||||
|
public const string DynamicHeaderCookie = "cxsparse";
|
||||||
|
}
|
||||||
50
src/VHDPatchFix/VhdFooter.cs
Normal file
50
src/VHDPatchFix/VhdFooter.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public sealed record VhdFooter(byte[] Bytes, ulong DataOffset, uint DiskType)
|
||||||
|
{
|
||||||
|
public static VhdFooter Read(Stream stream)
|
||||||
|
{
|
||||||
|
if (stream.Length < VhdConstants.FooterSize)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("File is too small to contain a VHD footer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = new byte[VhdConstants.FooterSize];
|
||||||
|
stream.Position = stream.Length - VhdConstants.FooterSize;
|
||||||
|
ReadExactly(stream, bytes);
|
||||||
|
|
||||||
|
string cookie = Encoding.ASCII.GetString(bytes, 0, 8);
|
||||||
|
if (cookie != VhdConstants.FooterCookie)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("The file does not end with a classic VHD footer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint storedChecksum = Endian.ReadUInt32(bytes, 64);
|
||||||
|
Endian.WriteUInt32(bytes, 64, 0);
|
||||||
|
uint computedChecksum = VhdChecksum.Compute(bytes);
|
||||||
|
Endian.WriteUInt32(bytes, 64, storedChecksum);
|
||||||
|
if (storedChecksum != computedChecksum)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("The VHD footer checksum is invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VhdFooter(bytes, Endian.ReadUInt64(bytes, 16), Endian.ReadUInt32(bytes, 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReadExactly(Stream stream, byte[] buffer)
|
||||||
|
{
|
||||||
|
int read = 0;
|
||||||
|
while (read < buffer.Length)
|
||||||
|
{
|
||||||
|
int count = stream.Read(buffer, read, buffer.Length - read);
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
|
||||||
|
read += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/VHDPatchFix/VhdParentPathPatcher.cs
Normal file
141
src/VHDPatchFix/VhdParentPathPatcher.cs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public static class VhdParentPathPatcher
|
||||||
|
{
|
||||||
|
public static PatchResult Patch(PatchOptions options)
|
||||||
|
{
|
||||||
|
if (!File.Exists(options.VhdPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("VHD file was not found.", options.VhdPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using FileStream stream = new(options.VhdPath, options.DryRun ? FileMode.Open : FileMode.Open, options.DryRun ? FileAccess.Read : FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
VhdFooter footer = VhdFooter.Read(stream);
|
||||||
|
if (footer.DiskType != VhdConstants.DifferencingDiskType)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Only classic VHD differencing disks are supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicHeader header = DynamicHeader.Read(stream, footer.DataOffset);
|
||||||
|
ParentLocator[] locators = header.ParentLocators.Where(locator => locator.IsPresent && locator.IsWindowsUnicode).ToArray();
|
||||||
|
if (locators.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("No Windows unicode parent locator was found in the VHD dynamic header.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? oldParentPath = locators
|
||||||
|
.Select(locator => VhdParentPathReader.ReadLocatorPath(stream, locator))
|
||||||
|
.FirstOrDefault(path => !string.IsNullOrWhiteSpace(path)) ?? header.ParentUnicodeName;
|
||||||
|
|
||||||
|
byte[] newLocatorData = VhdParentPathReader.EncodeWindowsLocatorPath(options.ParentPath);
|
||||||
|
bool needsRelocation = locators.Any(locator => newLocatorData.Length > CheckedCapacity(locator));
|
||||||
|
if (options.DryRun)
|
||||||
|
{
|
||||||
|
return new PatchResult(options.VhdPath, options.ParentPath, oldParentPath, null, true, needsRelocation, locators.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
string backupPath = CreateBackup(options);
|
||||||
|
header.SetParentUnicodeName(options.ParentPath);
|
||||||
|
|
||||||
|
foreach (ParentLocator locator in locators)
|
||||||
|
{
|
||||||
|
ParentLocator updated = newLocatorData.Length <= CheckedCapacity(locator)
|
||||||
|
? WriteInPlace(stream, locator, newLocatorData)
|
||||||
|
: WriteRelocated(stream, footer, locator, newLocatorData);
|
||||||
|
header.SetLocator(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
header.UpdateChecksum();
|
||||||
|
stream.Position = header.Offset;
|
||||||
|
stream.Write(header.Bytes);
|
||||||
|
stream.Flush(true);
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
VhdFooter verifyFooter = VhdFooter.Read(stream);
|
||||||
|
DynamicHeader verifyHeader = DynamicHeader.Read(stream, verifyFooter.DataOffset);
|
||||||
|
ParentLocator? verifyLocator = verifyHeader.ParentLocators.FirstOrDefault(locator => locator.IsPresent && locator.IsWindowsUnicode);
|
||||||
|
string? verifiedPath = verifyLocator is null ? null : VhdParentPathReader.ReadLocatorPath(stream, verifyLocator);
|
||||||
|
if (!string.Equals(verifiedPath, options.ParentPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("The VHD was written, but verification did not read back the requested parent path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PatchResult(options.VhdPath, options.ParentPath, oldParentPath, backupPath, false, needsRelocation, locators.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateBackup(PatchOptions options)
|
||||||
|
{
|
||||||
|
if (options.NoBackup)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
string backupPath = options.BackupPath ?? options.VhdPath + ".bak";
|
||||||
|
if (File.Exists(backupPath))
|
||||||
|
{
|
||||||
|
throw new IOException($"Backup path already exists: {backupPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(options.VhdPath, backupPath);
|
||||||
|
return backupPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParentLocator WriteInPlace(Stream stream, ParentLocator locator, byte[] data)
|
||||||
|
{
|
||||||
|
uint capacity = CheckedCapacity(locator);
|
||||||
|
stream.Position = (long)locator.PlatformDataOffset;
|
||||||
|
stream.Write(data);
|
||||||
|
WriteZeros(stream, checked((int)(capacity - data.Length)));
|
||||||
|
return locator with { PlatformDataLength = (uint)data.Length };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParentLocator WriteRelocated(Stream stream, VhdFooter footer, ParentLocator locator, byte[] data)
|
||||||
|
{
|
||||||
|
long footerOffset = stream.Length - VhdConstants.FooterSize;
|
||||||
|
stream.SetLength(footerOffset);
|
||||||
|
long dataOffset = Align(stream.Length, VhdConstants.SectorSize);
|
||||||
|
stream.Position = stream.Length;
|
||||||
|
WriteZeros(stream, checked((int)(dataOffset - stream.Length)));
|
||||||
|
stream.Position = dataOffset;
|
||||||
|
stream.Write(data);
|
||||||
|
|
||||||
|
uint dataSpace = checked((uint)Align(data.Length, VhdConstants.SectorSize));
|
||||||
|
long paddedEnd = dataOffset + dataSpace;
|
||||||
|
WriteZeros(stream, checked((int)(paddedEnd - stream.Position)));
|
||||||
|
stream.Write(footer.Bytes);
|
||||||
|
|
||||||
|
return locator with
|
||||||
|
{
|
||||||
|
PlatformDataOffset = (ulong)dataOffset,
|
||||||
|
PlatformDataSpace = dataSpace,
|
||||||
|
PlatformDataLength = (uint)data.Length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint CheckedCapacity(ParentLocator locator)
|
||||||
|
{
|
||||||
|
return locator.PlatformDataSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long Align(long value, int alignment)
|
||||||
|
{
|
||||||
|
long remainder = value % alignment;
|
||||||
|
return remainder == 0 ? value : value + alignment - remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteZeros(Stream stream, int count)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] zeros = new byte[Math.Min(count, 8192)];
|
||||||
|
while (count > 0)
|
||||||
|
{
|
||||||
|
int toWrite = Math.Min(count, zeros.Length);
|
||||||
|
stream.Write(zeros, 0, toWrite);
|
||||||
|
count -= toWrite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/VHDPatchFix/VhdParentPathReader.cs
Normal file
91
src/VHDPatchFix/VhdParentPathReader.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace VHDPatchFix;
|
||||||
|
|
||||||
|
public static class VhdParentPathReader
|
||||||
|
{
|
||||||
|
public static string? ReadLocatorPath(Stream stream, ParentLocator locator)
|
||||||
|
{
|
||||||
|
if (!locator.IsPresent || locator.PlatformDataLength == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locator.PlatformDataOffset > long.MaxValue ||
|
||||||
|
locator.PlatformDataOffset + locator.PlatformDataLength > (ulong)stream.Length)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = new byte[locator.PlatformDataLength];
|
||||||
|
stream.Position = (long)locator.PlatformDataOffset;
|
||||||
|
ReadExactly(stream, bytes);
|
||||||
|
return DecodeLocatorPath(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DecodeLocatorPath(ReadOnlySpan<byte> bytes)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<byte> trimmed = TrimTrailingZeros(bytes);
|
||||||
|
string littleEndian = Encoding.Unicode.GetString(trimmed);
|
||||||
|
if (LooksLikePath(littleEndian))
|
||||||
|
{
|
||||||
|
return littleEndian.TrimEnd('\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
string bigEndian = Encoding.BigEndianUnicode.GetString(trimmed);
|
||||||
|
if (LooksLikePath(bigEndian))
|
||||||
|
{
|
||||||
|
return bigEndian.TrimEnd('\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(trimmed).TrimEnd('\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] EncodeWindowsLocatorPath(string parentPath)
|
||||||
|
{
|
||||||
|
return Encoding.Unicode.GetBytes(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<byte> TrimTrailingZeros(ReadOnlySpan<byte> bytes)
|
||||||
|
{
|
||||||
|
int length = bytes.Length;
|
||||||
|
while (length > 0 && bytes[length - 1] == 0)
|
||||||
|
{
|
||||||
|
length--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length % 2 != 0 && length < bytes.Length)
|
||||||
|
{
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.Slice(0, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikePath(string value)
|
||||||
|
{
|
||||||
|
string trimmed = value.TrimEnd('\0');
|
||||||
|
if (trimmed.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int controlChars = trimmed.Count(char.IsControl);
|
||||||
|
return controlChars == 0 && (trimmed.Contains('\\') || trimmed.Contains('/') || trimmed.Contains(':'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReadExactly(Stream stream, byte[] buffer)
|
||||||
|
{
|
||||||
|
int read = 0;
|
||||||
|
while (read < buffer.Length)
|
||||||
|
{
|
||||||
|
int count = stream.Read(buffer, read, buffer.Length - read);
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
|
||||||
|
read += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// <autogenerated />
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("vhdfix")]
|
||||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("vhdfix")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("vhdfix")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|
||||||
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// <auto-generated/>
|
||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.IO;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Net.Http;
|
||||||
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// <autogenerated />
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.AssemblyCompanyAttribute("vhdfix")]
|
||||||
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||||
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||||
|
[assembly: System.Reflection.AssemblyProductAttribute("vhdfix")]
|
||||||
|
[assembly: System.Reflection.AssemblyTitleAttribute("vhdfix")]
|
||||||
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|
||||||
|
// Generated by the MSBuild WriteCodeFragment class.
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// <auto-generated/>
|
||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.IO;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Net.Http;
|
||||||
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
Reference in New Issue
Block a user