commit 5c1aa530587748d5dbef6848f0021e2df3ba943c Author: zhicheng233 Date: Fri May 29 23:58:49 2026 +0800 frist commit diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..332adc5 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..35c2fd7 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/VHDPatchFix.sln b/VHDPatchFix.sln new file mode 100644 index 0000000..2f84f91 --- /dev/null +++ b/VHDPatchFix.sln @@ -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 diff --git a/src/VHDPatchFix/.idea/.idea.VHDPatchFix.dir/.idea/.gitignore b/src/VHDPatchFix/.idea/.idea.VHDPatchFix.dir/.idea/.gitignore new file mode 100644 index 0000000..9e60b27 --- /dev/null +++ b/src/VHDPatchFix/.idea/.idea.VHDPatchFix.dir/.idea/.gitignore @@ -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 diff --git a/src/VHDPatchFix/DynamicHeader.cs b/src/VHDPatchFix/DynamicHeader.cs new file mode 100644 index 0000000..2c097a9 --- /dev/null +++ b/src/VHDPatchFix/DynamicHeader.cs @@ -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; + } + } +} diff --git a/src/VHDPatchFix/Endian.cs b/src/VHDPatchFix/Endian.cs new file mode 100644 index 0000000..bd3446d --- /dev/null +++ b/src/VHDPatchFix/Endian.cs @@ -0,0 +1,26 @@ +using System.Buffers.Binary; + +namespace VHDPatchFix; + +public static class Endian +{ + public static uint ReadUInt32(ReadOnlySpan bytes, int offset) + { + return BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(offset, 4)); + } + + public static ulong ReadUInt64(ReadOnlySpan bytes, int offset) + { + return BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(offset, 8)); + } + + public static void WriteUInt32(Span bytes, int offset, uint value) + { + BinaryPrimitives.WriteUInt32BigEndian(bytes.Slice(offset, 4), value); + } + + public static void WriteUInt64(Span bytes, int offset, ulong value) + { + BinaryPrimitives.WriteUInt64BigEndian(bytes.Slice(offset, 8), value); + } +} diff --git a/src/VHDPatchFix/ParentLocator.cs b/src/VHDPatchFix/ParentLocator.cs new file mode 100644 index 0000000..8a1b5b8 --- /dev/null +++ b/src/VHDPatchFix/ParentLocator.cs @@ -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 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 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 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); + } +} diff --git a/src/VHDPatchFix/PatchOptions.cs b/src/VHDPatchFix/PatchOptions.cs new file mode 100644 index 0000000..a7be6df --- /dev/null +++ b/src/VHDPatchFix/PatchOptions.cs @@ -0,0 +1,9 @@ +namespace VHDPatchFix; + +public sealed record PatchOptions( + string VhdPath, + string ParentPath, + bool DryRun, + bool NoBackup, + string? BackupPath, + bool Verbose); diff --git a/src/VHDPatchFix/PatchResult.cs b/src/VHDPatchFix/PatchResult.cs new file mode 100644 index 0000000..9361929 --- /dev/null +++ b/src/VHDPatchFix/PatchResult.cs @@ -0,0 +1,10 @@ +namespace VHDPatchFix; + +public sealed record PatchResult( + string VhdPath, + string NewParentPath, + string? OldParentPath, + string? BackupPath, + bool DryRun, + bool UsedRelocation, + int UpdatedLocatorCount); diff --git a/src/VHDPatchFix/Program.cs b/src/VHDPatchFix/Program.cs new file mode 100644 index 0000000..14ab2d5 --- /dev/null +++ b/src/VHDPatchFix/Program.cs @@ -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 "); + } + + if (string.IsNullOrWhiteSpace(parentPath)) + { + throw new ArgumentException("Missing required argument: --parent "); + } + + 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 --parent [--dry-run] [--no-backup] [--backup-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."); + } + } +} diff --git a/src/VHDPatchFix/VHDPatchFix.csproj b/src/VHDPatchFix/VHDPatchFix.csproj new file mode 100644 index 0000000..d43fd50 --- /dev/null +++ b/src/VHDPatchFix/VHDPatchFix.csproj @@ -0,0 +1,9 @@ + + + Exe + net10.0 + enable + enable + vhdfix + + diff --git a/src/VHDPatchFix/VhdChecksum.cs b/src/VHDPatchFix/VhdChecksum.cs new file mode 100644 index 0000000..49582cc --- /dev/null +++ b/src/VHDPatchFix/VhdChecksum.cs @@ -0,0 +1,15 @@ +namespace VHDPatchFix; + +public static class VhdChecksum +{ + public static uint Compute(ReadOnlySpan bytes) + { + uint sum = 0; + foreach (byte value in bytes) + { + sum += value; + } + + return ~sum; + } +} diff --git a/src/VHDPatchFix/VhdConstants.cs b/src/VHDPatchFix/VhdConstants.cs new file mode 100644 index 0000000..5b4accc --- /dev/null +++ b/src/VHDPatchFix/VhdConstants.cs @@ -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"; +} diff --git a/src/VHDPatchFix/VhdFooter.cs b/src/VHDPatchFix/VhdFooter.cs new file mode 100644 index 0000000..3ed5373 --- /dev/null +++ b/src/VHDPatchFix/VhdFooter.cs @@ -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; + } + } +} diff --git a/src/VHDPatchFix/VhdParentPathPatcher.cs b/src/VHDPatchFix/VhdParentPathPatcher.cs new file mode 100644 index 0000000..1c5e63b --- /dev/null +++ b/src/VHDPatchFix/VhdParentPathPatcher.cs @@ -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; + } + } +} diff --git a/src/VHDPatchFix/VhdParentPathReader.cs b/src/VHDPatchFix/VhdParentPathReader.cs new file mode 100644 index 0000000..bfdaf14 --- /dev/null +++ b/src/VHDPatchFix/VhdParentPathReader.cs @@ -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 bytes) + { + ReadOnlySpan 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 TrimTrailingZeros(ReadOnlySpan 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; + } + } +} diff --git a/src/VHDPatchFix/obj/Debug/net10.0/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs b/src/VHDPatchFix/obj/Debug/net10.0/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs new file mode 100644 index 0000000..925b135 --- /dev/null +++ b/src/VHDPatchFix/obj/Debug/net10.0/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")] diff --git a/src/VHDPatchFix/obj/Debug/net10.0/VHDPatchFix.AssemblyInfo.cs b/src/VHDPatchFix/obj/Debug/net10.0/VHDPatchFix.AssemblyInfo.cs new file mode 100644 index 0000000..1402d71 --- /dev/null +++ b/src/VHDPatchFix/obj/Debug/net10.0/VHDPatchFix.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +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. + diff --git a/src/VHDPatchFix/obj/Debug/net10.0/VHDPatchFix.GlobalUsings.g.cs b/src/VHDPatchFix/obj/Debug/net10.0/VHDPatchFix.GlobalUsings.g.cs new file mode 100644 index 0000000..d12bcbc --- /dev/null +++ b/src/VHDPatchFix/obj/Debug/net10.0/VHDPatchFix.GlobalUsings.g.cs @@ -0,0 +1,8 @@ +// +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; diff --git a/src/VHDPatchFix/obj/Release/net10.0/win-x64/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs b/src/VHDPatchFix/obj/Release/net10.0/win-x64/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs new file mode 100644 index 0000000..925b135 --- /dev/null +++ b/src/VHDPatchFix/obj/Release/net10.0/win-x64/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")] diff --git a/src/VHDPatchFix/obj/Release/net10.0/win-x64/VHDPatchFix.AssemblyInfo.cs b/src/VHDPatchFix/obj/Release/net10.0/win-x64/VHDPatchFix.AssemblyInfo.cs new file mode 100644 index 0000000..3f6c37c --- /dev/null +++ b/src/VHDPatchFix/obj/Release/net10.0/win-x64/VHDPatchFix.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +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. + diff --git a/src/VHDPatchFix/obj/Release/net10.0/win-x64/VHDPatchFix.GlobalUsings.g.cs b/src/VHDPatchFix/obj/Release/net10.0/win-x64/VHDPatchFix.GlobalUsings.g.cs new file mode 100644 index 0000000..d12bcbc --- /dev/null +++ b/src/VHDPatchFix/obj/Release/net10.0/win-x64/VHDPatchFix.GlobalUsings.g.cs @@ -0,0 +1,8 @@ +// +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;