mirror of
https://github.com/jamesstringer90/appsandbox.git
synced 2026-10-23 11:33:37 -05:00
1596 lines
54 KiB
C
1596 lines
54 KiB
C
/*
|
|
* iso-patch.exe -- Windows ISO utilities:
|
|
*
|
|
* iso-patch.exe <path-to-windows.iso>
|
|
* Rebuild ISO with noprompt UEFI boot files (no "Press Any Key").
|
|
* Output: <exe-dir>\<basename>_noprompt.iso
|
|
*
|
|
* iso-patch.exe --to-vhdx <iso> [image-index] [size-gb] [--output path] [--stage manifest]
|
|
* Convert a Windows installer ISO into a bootable UEFI VHDX.
|
|
* Output defaults to <exe-dir>\<basename>.vhdx, or --output overrides.
|
|
* image-index defaults to 1, size-gb defaults to 64 (min 16).
|
|
* --stage: copy files listed in manifest onto the VHDX.
|
|
* Manifest: tab-separated lines of <source>\t<dest_relative_to_root>
|
|
*/
|
|
|
|
#include <windows.h>
|
|
#include <virtdisk.h>
|
|
#include <winioctl.h>
|
|
#include <imapi2fs.h>
|
|
#include <shlwapi.h>
|
|
#include <stdio.h>
|
|
|
|
#pragma comment(lib, "virtdisk.lib")
|
|
#pragma comment(lib, "ole32.lib")
|
|
#pragma comment(lib, "oleaut32.lib")
|
|
#pragma comment(lib, "shlwapi.lib")
|
|
|
|
/* ---- GUIDs ---- */
|
|
/* Use custom names to avoid clashing with EXTERN_C declarations in imapi2fs.h */
|
|
|
|
static const GUID CLSID_FSImage =
|
|
{0x2C941FC5, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}};
|
|
static const GUID IID_IFSImage =
|
|
{0x2C941FE1, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}};
|
|
static const GUID CLSID_BootOpts =
|
|
{0x2C941FCE, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}};
|
|
static const GUID IID_IBootOpts =
|
|
{0x2C941FD4, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}};
|
|
static const GUID IID_IFsiDirItem =
|
|
{0x2C941FDC, 0x975B, 0x59BE, {0xA9, 0x60, 0x9A, 0x2A, 0x26, 0x28, 0x53, 0xA5}};
|
|
|
|
static const GUID VHDX_VENDOR_MS = {
|
|
0xec984aec, 0xa0f9, 0x47e9,
|
|
{ 0x90, 0x1f, 0x71, 0x41, 0x5a, 0x66, 0x34, 0x5b }
|
|
};
|
|
|
|
/* ---- Helpers ---- */
|
|
|
|
/* Output protocol (machine-parseable, one per line):
|
|
STATUS:<message> — step change (shown in UI log)
|
|
PROGRESS:<pct>:<msg> — progress update (updates in place in UI)
|
|
ERROR:<message> — failure (shown in UI log as error)
|
|
DONE:<path> — success, path to output file
|
|
*/
|
|
|
|
static void log_msg(const wchar_t *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
wprintf(L"STATUS:");
|
|
vwprintf(fmt, ap);
|
|
wprintf(L"\n");
|
|
fflush(stdout);
|
|
va_end(ap);
|
|
}
|
|
|
|
static void log_progress(int pct, const wchar_t *step)
|
|
{
|
|
wprintf(L"PROGRESS:%d:%s\n", pct, step);
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void log_done(const wchar_t *path)
|
|
{
|
|
wprintf(L"DONE:%s\n", path);
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void log_err(const wchar_t *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
fwprintf(stderr, L"ERROR:");
|
|
vfwprintf(stderr, fmt, ap);
|
|
fwprintf(stderr, L"\n");
|
|
fflush(stderr);
|
|
va_end(ap);
|
|
}
|
|
|
|
/* Build output path: <exe_dir>\<basename><suffix> */
|
|
static void build_output_path(wchar_t *out, size_t out_len,
|
|
const wchar_t *input_path, const wchar_t *suffix)
|
|
{
|
|
wchar_t exe_dir[MAX_PATH];
|
|
const wchar_t *base_name;
|
|
wchar_t stem[MAX_PATH];
|
|
wchar_t *slash, *dot;
|
|
|
|
GetModuleFileNameW(NULL, exe_dir, MAX_PATH);
|
|
slash = wcsrchr(exe_dir, L'\\');
|
|
if (slash) *slash = L'\0';
|
|
|
|
base_name = wcsrchr(input_path, L'\\');
|
|
base_name = base_name ? base_name + 1 : input_path;
|
|
wcscpy_s(stem, MAX_PATH, base_name);
|
|
dot = wcsrchr(stem, L'.');
|
|
if (dot) *dot = L'\0';
|
|
|
|
swprintf_s(out, out_len, L"%s\\%s%s", exe_dir, stem, suffix);
|
|
}
|
|
|
|
/* Run a command and wait for it to finish.
|
|
If quiet=TRUE, child stdout/stderr are suppressed.
|
|
Returns process exit code, or -1 on failure. */
|
|
static int run_command(const wchar_t *cmdline, BOOL quiet)
|
|
{
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
DWORD exit_code = (DWORD)-1;
|
|
wchar_t *cmd_buf;
|
|
size_t len;
|
|
HANDLE hNul = INVALID_HANDLE_VALUE;
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (quiet) {
|
|
SECURITY_ATTRIBUTES sa;
|
|
sa.nLength = sizeof(sa);
|
|
sa.lpSecurityDescriptor = NULL;
|
|
sa.bInheritHandle = TRUE;
|
|
hNul = CreateFileW(L"NUL", GENERIC_WRITE, FILE_SHARE_WRITE,
|
|
&sa, OPEN_EXISTING, 0, NULL);
|
|
if (hNul != INVALID_HANDLE_VALUE) {
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
si.hStdOutput = hNul;
|
|
si.hStdError = hNul;
|
|
}
|
|
}
|
|
|
|
/* CreateProcessW needs a mutable command line */
|
|
len = wcslen(cmdline) + 1;
|
|
cmd_buf = (wchar_t *)malloc(len * sizeof(wchar_t));
|
|
if (!cmd_buf) {
|
|
if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
|
|
return -1;
|
|
}
|
|
wcscpy_s(cmd_buf, len, cmdline);
|
|
|
|
if (!CreateProcessW(NULL, cmd_buf, NULL, NULL, quiet ? TRUE : FALSE,
|
|
0, NULL, NULL, &si, &pi)) {
|
|
log_err(L"Failed to run: %s (error %lu)", cmdline, GetLastError());
|
|
free(cmd_buf);
|
|
if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
|
|
return -1;
|
|
}
|
|
free(cmd_buf);
|
|
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
|
|
return (int)exit_code;
|
|
}
|
|
|
|
/* Run a command, capture stdout, and report percentage progress.
|
|
Scans output for patterns like "45.0%" and calls log_msg with updates.
|
|
prefix: status message shown before the percentage (e.g. "Applying Windows image").
|
|
Returns process exit code, or -1 on failure. */
|
|
static int run_command_with_progress(const wchar_t *cmdline, const wchar_t *prefix)
|
|
{
|
|
SECURITY_ATTRIBUTES sa;
|
|
HANDLE hReadPipe = NULL, hWritePipe = NULL;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
DWORD exit_code = (DWORD)-1;
|
|
wchar_t *cmd_buf;
|
|
size_t len;
|
|
int last_pct = -1;
|
|
|
|
sa.nLength = sizeof(sa);
|
|
sa.lpSecurityDescriptor = NULL;
|
|
sa.bInheritHandle = TRUE;
|
|
|
|
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0))
|
|
return -1;
|
|
|
|
/* Ensure the read handle is not inherited */
|
|
SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
si.hStdOutput = hWritePipe;
|
|
si.hStdError = hWritePipe;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
len = wcslen(cmdline) + 1;
|
|
cmd_buf = (wchar_t *)malloc(len * sizeof(wchar_t));
|
|
if (!cmd_buf) {
|
|
CloseHandle(hReadPipe);
|
|
CloseHandle(hWritePipe);
|
|
return -1;
|
|
}
|
|
wcscpy_s(cmd_buf, len, cmdline);
|
|
|
|
if (!CreateProcessW(NULL, cmd_buf, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
|
|
log_err(L"Failed to run: %s (error %lu)", cmdline, GetLastError());
|
|
free(cmd_buf);
|
|
CloseHandle(hReadPipe);
|
|
CloseHandle(hWritePipe);
|
|
return -1;
|
|
}
|
|
free(cmd_buf);
|
|
|
|
/* Close write end — we only read */
|
|
CloseHandle(hWritePipe);
|
|
hWritePipe = NULL;
|
|
|
|
/* Read output and scan for percentage */
|
|
{
|
|
char buf[4096];
|
|
DWORD bytes_read;
|
|
while (ReadFile(hReadPipe, buf, sizeof(buf) - 1, &bytes_read, NULL) && bytes_read > 0) {
|
|
buf[bytes_read] = '\0';
|
|
/* Scan for XX.X% pattern */
|
|
for (DWORD i = 0; i < bytes_read; i++) {
|
|
if (buf[i] == '%') {
|
|
/* Walk backwards to find the number */
|
|
int j = (int)i - 1;
|
|
while (j >= 0 && (buf[j] == '.' || (buf[j] >= '0' && buf[j] <= '9')))
|
|
j--;
|
|
j++;
|
|
if (j < (int)i) {
|
|
/* Parse the percentage as integer */
|
|
double pct_val = atof(buf + j);
|
|
int pct = (int)pct_val;
|
|
if (pct >= 0 && pct <= 100 && pct != last_pct) {
|
|
last_pct = pct;
|
|
log_progress(pct, prefix);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CloseHandle(hReadPipe);
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return (int)exit_code;
|
|
}
|
|
|
|
/* Run a command and capture stdout into a caller-provided buffer.
|
|
Returns process exit code, or -1 on failure. */
|
|
static int run_command_capture(const wchar_t *cmdline, char *out_buf, int out_buf_size)
|
|
{
|
|
SECURITY_ATTRIBUTES sa;
|
|
HANDLE hReadPipe = NULL, hWritePipe = NULL;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
DWORD exit_code = (DWORD)-1;
|
|
wchar_t *cmd_buf;
|
|
size_t len;
|
|
int total = 0;
|
|
|
|
sa.nLength = sizeof(sa);
|
|
sa.lpSecurityDescriptor = NULL;
|
|
sa.bInheritHandle = TRUE;
|
|
|
|
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0))
|
|
return -1;
|
|
|
|
SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
si.hStdOutput = hWritePipe;
|
|
si.hStdError = hWritePipe;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
len = wcslen(cmdline) + 1;
|
|
cmd_buf = (wchar_t *)malloc(len * sizeof(wchar_t));
|
|
if (!cmd_buf) {
|
|
CloseHandle(hReadPipe);
|
|
CloseHandle(hWritePipe);
|
|
return -1;
|
|
}
|
|
wcscpy_s(cmd_buf, len, cmdline);
|
|
|
|
if (!CreateProcessW(NULL, cmd_buf, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
|
|
free(cmd_buf);
|
|
CloseHandle(hReadPipe);
|
|
CloseHandle(hWritePipe);
|
|
return -1;
|
|
}
|
|
free(cmd_buf);
|
|
|
|
CloseHandle(hWritePipe);
|
|
hWritePipe = NULL;
|
|
|
|
/* Read all output into buffer */
|
|
{
|
|
DWORD bytes_read;
|
|
while (total < out_buf_size - 1 &&
|
|
ReadFile(hReadPipe, out_buf + total,
|
|
(DWORD)(out_buf_size - 1 - total), &bytes_read, NULL) &&
|
|
bytes_read > 0) {
|
|
total += (int)bytes_read;
|
|
}
|
|
out_buf[total] = '\0';
|
|
}
|
|
|
|
CloseHandle(hReadPipe);
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return (int)exit_code;
|
|
}
|
|
|
|
/* Snapshot of which drive letters are CDROM before mounting */
|
|
static DWORD snapshot_cdrom_drives(void)
|
|
{
|
|
DWORD mask = 0;
|
|
wchar_t root[4] = L"A:\\";
|
|
for (int i = 0; i < 26; i++) {
|
|
root[0] = (wchar_t)(L'A' + i);
|
|
if (GetDriveTypeW(root) == DRIVE_CDROM)
|
|
mask |= (1u << i);
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
/* Find a new CDROM drive letter that appeared after mounting */
|
|
static wchar_t find_new_cdrom(DWORD before_mask)
|
|
{
|
|
wchar_t root[4] = L"A:\\";
|
|
for (int i = 0; i < 26; i++) {
|
|
root[0] = (wchar_t)(L'A' + i);
|
|
if (!(before_mask & (1u << i)) && GetDriveTypeW(root) == DRIVE_CDROM)
|
|
return (wchar_t)(L'A' + i);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Write an IStream to a file */
|
|
static HRESULT write_stream_to_file(IStream *stream, const wchar_t *path)
|
|
{
|
|
HANDLE hFile;
|
|
BYTE buf[65536];
|
|
ULONG bytes_read;
|
|
DWORD bytes_written;
|
|
HRESULT hr;
|
|
LARGE_INTEGER zero;
|
|
|
|
hFile = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
|
|
if (hFile == INVALID_HANDLE_VALUE)
|
|
return HRESULT_FROM_WIN32(GetLastError());
|
|
|
|
zero.QuadPart = 0;
|
|
stream->lpVtbl->Seek(stream, zero, STREAM_SEEK_SET, NULL);
|
|
|
|
for (;;) {
|
|
hr = stream->lpVtbl->Read(stream, buf, sizeof(buf), &bytes_read);
|
|
if (FAILED(hr) || bytes_read == 0) break;
|
|
if (!WriteFile(hFile, buf, bytes_read, &bytes_written, NULL)) {
|
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
|
CloseHandle(hFile);
|
|
return hr;
|
|
}
|
|
}
|
|
|
|
CloseHandle(hFile);
|
|
return S_OK;
|
|
}
|
|
|
|
/* Navigate a directory tree: root -> child1 -> child2 -> ... -> NULL
|
|
Returns the deepest IFsiDirectoryItem. Caller must Release(). */
|
|
static HRESULT navigate_fsi_path(IFsiDirectoryItem *root, IFsiDirectoryItem **out,
|
|
const wchar_t *name1, ...)
|
|
{
|
|
va_list ap;
|
|
IFsiDirectoryItem *current = root;
|
|
IFsiItem *item = NULL;
|
|
const wchar_t *name = name1;
|
|
HRESULT hr = S_OK;
|
|
BSTR bstr;
|
|
|
|
current->lpVtbl->AddRef(current);
|
|
|
|
va_start(ap, name1);
|
|
while (name) {
|
|
bstr = SysAllocString(name);
|
|
hr = current->lpVtbl->get_Item(current, bstr, &item);
|
|
SysFreeString(bstr);
|
|
if (FAILED(hr)) {
|
|
current->lpVtbl->Release(current);
|
|
va_end(ap);
|
|
return hr;
|
|
}
|
|
|
|
IFsiDirectoryItem *next = NULL;
|
|
hr = item->lpVtbl->QueryInterface(item, &IID_IFsiDirItem, (void **)&next);
|
|
item->lpVtbl->Release(item);
|
|
if (FAILED(hr)) {
|
|
current->lpVtbl->Release(current);
|
|
va_end(ap);
|
|
return hr;
|
|
}
|
|
|
|
current->lpVtbl->Release(current);
|
|
current = next;
|
|
name = va_arg(ap, const wchar_t *);
|
|
}
|
|
va_end(ap);
|
|
|
|
*out = current;
|
|
return S_OK;
|
|
}
|
|
|
|
/* ======================================================================
|
|
* --to-vhdx: Convert Windows installer ISO to bootable UEFI VHDX
|
|
* ====================================================================== */
|
|
|
|
/* Partition sizes */
|
|
#define EFI_SIZE_MB 200
|
|
#define MSR_SIZE_MB 128
|
|
#define DEFAULT_VHDX_SIZE_GB 64
|
|
|
|
/* Find the volume GUID path for a partition on a given disk.
|
|
disk_number: the disk number from GetVirtualDiskPhysicalPath
|
|
partition_number: 1-based partition index in the GPT layout.
|
|
Pass 0 to match any volume on the disk (e.g. ISO with no partitions).
|
|
Returns TRUE and fills vol_path (e.g. \\?\Volume{guid}\) on success. */
|
|
static BOOL find_volume_for_partition(DWORD disk_number, DWORD partition_number,
|
|
wchar_t *vol_path, size_t vol_path_len)
|
|
{
|
|
HANDLE hFind;
|
|
wchar_t vol_name[MAX_PATH];
|
|
BOOL found = FALSE;
|
|
|
|
hFind = FindFirstVolumeW(vol_name, MAX_PATH);
|
|
if (hFind == INVALID_HANDLE_VALUE) return FALSE;
|
|
|
|
do {
|
|
/* Open the volume — strip trailing backslash for CreateFileW */
|
|
wchar_t vol_dev[MAX_PATH];
|
|
HANDLE hVol;
|
|
wcscpy_s(vol_dev, MAX_PATH, vol_name);
|
|
size_t vlen = wcslen(vol_dev);
|
|
if (vlen > 0 && vol_dev[vlen - 1] == L'\\')
|
|
vol_dev[vlen - 1] = L'\0';
|
|
|
|
hVol = CreateFileW(vol_dev, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
NULL, OPEN_EXISTING, 0, NULL);
|
|
if (hVol != INVALID_HANDLE_VALUE) {
|
|
/* Get the disk extents for this volume */
|
|
BYTE buf[256];
|
|
DWORD bytes;
|
|
if (DeviceIoControl(hVol, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
|
|
NULL, 0, buf, sizeof(buf), &bytes, NULL)) {
|
|
VOLUME_DISK_EXTENTS *ext = (VOLUME_DISK_EXTENTS *)buf;
|
|
if (ext->NumberOfDiskExtents >= 1 &&
|
|
ext->Extents[0].DiskNumber == disk_number) {
|
|
if (partition_number == 0) {
|
|
/* Match any volume on this disk (e.g. ISO) */
|
|
wcscpy_s(vol_path, vol_path_len, vol_name);
|
|
found = TRUE;
|
|
} else {
|
|
/* Check partition number via IOCTL_STORAGE_GET_DEVICE_NUMBER */
|
|
STORAGE_DEVICE_NUMBER sdn;
|
|
if (DeviceIoControl(hVol, IOCTL_STORAGE_GET_DEVICE_NUMBER,
|
|
NULL, 0, &sdn, sizeof(sdn), &bytes, NULL)) {
|
|
if (sdn.PartitionNumber == partition_number) {
|
|
wcscpy_s(vol_path, vol_path_len, vol_name);
|
|
found = TRUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CloseHandle(hVol);
|
|
}
|
|
if (found) break;
|
|
} while (FindNextVolumeW(hFind, vol_name, MAX_PATH));
|
|
|
|
FindVolumeClose(hFind);
|
|
return found;
|
|
}
|
|
|
|
/* Extract disk number from physical path like \\.\PhysicalDrive3 */
|
|
static DWORD get_disk_number_from_path(const wchar_t *phys_path)
|
|
{
|
|
const wchar_t *p = wcsrchr(phys_path, L'e'); /* PhysicalDriv'e' */
|
|
if (p && p[1] >= L'0' && p[1] <= L'9')
|
|
return (DWORD)_wtoi(p + 1);
|
|
/* Fallback: find last run of digits */
|
|
size_t len = wcslen(phys_path);
|
|
while (len > 0 && phys_path[len - 1] >= L'0' && phys_path[len - 1] <= L'9')
|
|
len--;
|
|
return (DWORD)_wtoi(phys_path + len);
|
|
}
|
|
|
|
/* Create a temporary directory under %TEMP% with given prefix.
|
|
Returns TRUE and fills path on success. Path includes trailing backslash. */
|
|
static BOOL create_temp_mount_dir(const wchar_t *prefix, wchar_t *path, size_t path_len)
|
|
{
|
|
wchar_t temp_dir[MAX_PATH];
|
|
DWORD len = GetTempPathW(MAX_PATH, temp_dir);
|
|
if (len == 0) return FALSE;
|
|
|
|
swprintf_s(path, path_len, L"%s%s_%u\\", temp_dir, prefix, GetCurrentProcessId());
|
|
|
|
if (!CreateDirectoryW(path, NULL)) {
|
|
if (GetLastError() != ERROR_ALREADY_EXISTS)
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/* Mount a volume to a directory (no drive letter). vol_path must have trailing backslash.
|
|
mount_dir must have trailing backslash and must be an empty directory. */
|
|
static BOOL mount_volume_to_dir(const wchar_t *vol_path, const wchar_t *mount_dir)
|
|
{
|
|
return SetVolumeMountPointW(mount_dir, vol_path);
|
|
}
|
|
|
|
/* Unmount a volume from a directory. */
|
|
static BOOL unmount_volume_from_dir(const wchar_t *mount_dir)
|
|
{
|
|
return DeleteVolumeMountPointW(mount_dir);
|
|
}
|
|
|
|
/* Format a volume using format.exe. fs_type is "FAT32" or "NTFS".
|
|
vol_path is e.g. \\?\Volume{guid}\ — but format.exe needs a drive letter
|
|
or mount point. We pass the mount_dir which has a trailing backslash. */
|
|
static BOOL format_volume(const wchar_t *mount_dir, const wchar_t *fs_type, const wchar_t *label)
|
|
{
|
|
wchar_t cmd[512];
|
|
wchar_t sys_dir[MAX_PATH];
|
|
int ret;
|
|
|
|
GetSystemDirectoryW(sys_dir, MAX_PATH);
|
|
|
|
/* Strip trailing backslash — format.com doesn't like "path\" */
|
|
{
|
|
wchar_t mount_clean[MAX_PATH];
|
|
wcscpy_s(mount_clean, MAX_PATH, mount_dir);
|
|
size_t mlen = wcslen(mount_clean);
|
|
if (mlen > 0 && mount_clean[mlen - 1] == L'\\')
|
|
mount_clean[mlen - 1] = L'\0';
|
|
|
|
if (label && label[0]) {
|
|
swprintf_s(cmd, 512, L"%s\\format.com \"%s\" /FS:%s /Q /Y /V:%s",
|
|
sys_dir, mount_clean, fs_type, label);
|
|
} else {
|
|
swprintf_s(cmd, 512, L"%s\\format.com \"%s\" /FS:%s /Q /Y",
|
|
sys_dir, mount_clean, fs_type);
|
|
}
|
|
}
|
|
|
|
ret = run_command(cmd, TRUE);
|
|
if (ret != 0) {
|
|
log_err(L"Failed to format volume (exit code %d)", ret);
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/* Recursively create directories for a path.
|
|
path should be the full file path — directories are created for all
|
|
components except the last (the filename). */
|
|
static void ensure_parent_dirs(const wchar_t *file_path)
|
|
{
|
|
wchar_t dir[MAX_PATH];
|
|
wchar_t *p;
|
|
|
|
wcscpy_s(dir, MAX_PATH, file_path);
|
|
|
|
/* Find the last backslash (parent directory) */
|
|
p = wcsrchr(dir, L'\\');
|
|
if (!p) return;
|
|
*p = L'\0';
|
|
|
|
/* If directory already exists, done */
|
|
if (GetFileAttributesW(dir) != INVALID_FILE_ATTRIBUTES)
|
|
return;
|
|
|
|
/* Recursively create parent */
|
|
ensure_parent_dirs(dir);
|
|
CreateDirectoryW(dir, NULL);
|
|
}
|
|
|
|
/* Process a staging manifest file. Each line is tab-separated:
|
|
<source_path>\t<dest_path_relative_to_partition_root>
|
|
Lines starting with # are comments. Empty lines are skipped.
|
|
Returns the number of files staged, or -1 on error. */
|
|
static int stage_files(const wchar_t *manifest_path, const wchar_t *mount_root)
|
|
{
|
|
FILE *f = NULL;
|
|
wchar_t line[2048];
|
|
int count = 0;
|
|
int total = 0;
|
|
int skipped = 0;
|
|
|
|
if (_wfopen_s(&f, manifest_path, L"r, ccs=UTF-8") != 0 || !f) {
|
|
log_err(L"Cannot open staging manifest: %s", manifest_path);
|
|
return -1;
|
|
}
|
|
|
|
/* First pass: count lines for progress */
|
|
while (fgetws(line, 2048, f)) {
|
|
size_t len = wcslen(line);
|
|
while (len > 0 && (line[len - 1] == L'\n' || line[len - 1] == L'\r'))
|
|
line[--len] = L'\0';
|
|
if (len == 0 || line[0] == L'#') continue;
|
|
if (wcschr(line, L'\t')) total++;
|
|
}
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
/* Second pass: copy files */
|
|
while (fgetws(line, 2048, f)) {
|
|
wchar_t *src, *rel_dest, *tab;
|
|
wchar_t dest[MAX_PATH];
|
|
const wchar_t *filename;
|
|
size_t len = wcslen(line);
|
|
|
|
/* Strip newline */
|
|
while (len > 0 && (line[len - 1] == L'\n' || line[len - 1] == L'\r'))
|
|
line[--len] = L'\0';
|
|
if (len == 0 || line[0] == L'#') continue;
|
|
|
|
/* Skip UTF-8 BOM (U+FEFF) that ccs=UTF-8 may produce on first line */
|
|
if (line[0] == L'\xFEFF') {
|
|
memmove(line, line + 1, len * sizeof(wchar_t));
|
|
len--;
|
|
if (len == 0) continue;
|
|
}
|
|
|
|
/* Split on tab */
|
|
tab = wcschr(line, L'\t');
|
|
if (!tab) continue;
|
|
*tab = L'\0';
|
|
src = line;
|
|
rel_dest = tab + 1;
|
|
|
|
/* Skip leading backslash on relative dest */
|
|
if (rel_dest[0] == L'\\') rel_dest++;
|
|
|
|
/* Build full destination: mount_root + rel_dest */
|
|
swprintf_s(dest, MAX_PATH, L"%s%s", mount_root, rel_dest);
|
|
|
|
/* Extract just the filename for status */
|
|
filename = wcsrchr(rel_dest, L'\\');
|
|
filename = filename ? filename + 1 : rel_dest;
|
|
|
|
count++;
|
|
if (total > 0) {
|
|
int pct = (count * 100) / total;
|
|
log_progress(pct, L"Staging files...");
|
|
}
|
|
|
|
/* Create parent directories */
|
|
ensure_parent_dirs(dest);
|
|
|
|
log_msg(L"Copying: %s -> %s", src, dest);
|
|
if (!CopyFileW(src, dest, FALSE)) {
|
|
DWORD err = GetLastError();
|
|
/* Only fatal for core files (unattend, setup scripts, agent).
|
|
GPU/driver files are best-effort since Plan9 shares provide a fallback. */
|
|
if (wcsstr(rel_dest, L"HostDriverStore") != NULL ||
|
|
wcsstr(rel_dest, L"hostdriverstore") != NULL) {
|
|
log_msg(L"Warning: skipped %s (error %lu)", filename, err);
|
|
skipped++;
|
|
} else {
|
|
log_err(L"Failed to copy %s -> %s (error %lu)", src, dest, err);
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
if (skipped > 0)
|
|
log_msg(L"Warning: %d GPU driver file(s) skipped (will be copied by agent at boot)", skipped);
|
|
return count;
|
|
}
|
|
|
|
static int do_to_vhdx(const wchar_t *iso_path_arg, int image_index, int size_gb,
|
|
const wchar_t *manifest_path, const wchar_t *output_path_arg)
|
|
{
|
|
int exit_code = 1;
|
|
DWORD result;
|
|
wchar_t iso_path[MAX_PATH];
|
|
wchar_t vhdx_path[MAX_PATH];
|
|
|
|
/* ISO mount state */
|
|
HANDLE iso_handle = INVALID_HANDLE_VALUE;
|
|
wchar_t iso_drive = 0;
|
|
DWORD cdrom_before = 0;
|
|
wchar_t iso_root[4];
|
|
|
|
/* VHDX state */
|
|
HANDLE vhdx_handle = INVALID_HANDLE_VALUE;
|
|
HANDLE disk_handle = INVALID_HANDLE_VALUE;
|
|
wchar_t phys_path[MAX_PATH];
|
|
DWORD disk_number = 0;
|
|
|
|
/* Mount points */
|
|
wchar_t efi_mount[MAX_PATH] = {0};
|
|
wchar_t win_mount[MAX_PATH] = {0};
|
|
BOOL efi_mounted = FALSE;
|
|
BOOL win_mounted = FALSE;
|
|
|
|
/* WIM/ESD path found on ISO */
|
|
wchar_t wim_path[MAX_PATH];
|
|
|
|
/* Resolve ISO path */
|
|
if (!GetFullPathNameW(iso_path_arg, MAX_PATH, iso_path, NULL)) {
|
|
log_err(L"Invalid path: %s", iso_path_arg);
|
|
return 1;
|
|
}
|
|
if (GetFileAttributesW(iso_path) == INVALID_FILE_ATTRIBUTES) {
|
|
log_err(L"File not found: %s", iso_path);
|
|
return 1;
|
|
}
|
|
|
|
if (output_path_arg) {
|
|
if (!GetFullPathNameW(output_path_arg, MAX_PATH, vhdx_path, NULL)) {
|
|
log_err(L"Invalid output path: %s", output_path_arg);
|
|
return 1;
|
|
}
|
|
ensure_parent_dirs(vhdx_path);
|
|
} else {
|
|
build_output_path(vhdx_path, MAX_PATH, iso_path, L".vhdx");
|
|
}
|
|
|
|
/* ---- Step 1: Mount the ISO ---- */
|
|
log_msg(L"Mounting ISO...");
|
|
{
|
|
VIRTUAL_STORAGE_TYPE st;
|
|
OPEN_VIRTUAL_DISK_PARAMETERS op;
|
|
|
|
ZeroMemory(&st, sizeof(st));
|
|
st.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_ISO;
|
|
|
|
ZeroMemory(&op, sizeof(op));
|
|
op.Version = OPEN_VIRTUAL_DISK_VERSION_1;
|
|
|
|
cdrom_before = snapshot_cdrom_drives();
|
|
|
|
result = OpenVirtualDisk(&st, iso_path,
|
|
VIRTUAL_DISK_ACCESS_READ,
|
|
OPEN_VIRTUAL_DISK_FLAG_NONE,
|
|
&op, &iso_handle);
|
|
if (result != ERROR_SUCCESS) {
|
|
log_err(L"OpenVirtualDisk(ISO) failed (error %lu)", result);
|
|
goto cleanup;
|
|
}
|
|
|
|
result = AttachVirtualDisk(iso_handle, NULL,
|
|
ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY,
|
|
0, NULL, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
log_err(L"AttachVirtualDisk(ISO) failed (error %lu)", result);
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Wait for drive letter */
|
|
for (int i = 0; i < 20; i++) {
|
|
DWORD cdrom_now = snapshot_cdrom_drives();
|
|
DWORD new_drives = cdrom_now & ~cdrom_before;
|
|
if (new_drives) {
|
|
for (int b = 0; b < 26; b++) {
|
|
if (new_drives & (1u << b)) {
|
|
iso_drive = (wchar_t)(L'A' + b);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
Sleep(500);
|
|
}
|
|
if (!iso_drive) {
|
|
log_err(L"Mounted ISO but no drive letter appeared");
|
|
goto cleanup;
|
|
}
|
|
}
|
|
swprintf_s(iso_root, 4, L"%c:\\", iso_drive);
|
|
|
|
/* Wait for filesystem to be accessible */
|
|
{
|
|
wchar_t label[64];
|
|
int ready = 0;
|
|
for (int i = 0; i < 30; i++) {
|
|
if (GetVolumeInformationW(iso_root, label, 64, NULL, NULL, NULL, NULL, 0)) {
|
|
ready = 1;
|
|
break;
|
|
}
|
|
Sleep(500);
|
|
}
|
|
if (!ready) {
|
|
log_err(L"ISO volume never became accessible");
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* ---- Step 2: Find install.wim or install.esd ---- */
|
|
swprintf_s(wim_path, MAX_PATH, L"%c:\\sources\\install.wim", iso_drive);
|
|
if (GetFileAttributesW(wim_path) == INVALID_FILE_ATTRIBUTES) {
|
|
swprintf_s(wim_path, MAX_PATH, L"%c:\\sources\\install.esd", iso_drive);
|
|
if (GetFileAttributesW(wim_path) == INVALID_FILE_ATTRIBUTES) {
|
|
log_err(L"Neither install.wim nor install.esd found on ISO");
|
|
goto cleanup;
|
|
}
|
|
}
|
|
{
|
|
const wchar_t *wim_name = wcsrchr(wim_path, L'\\');
|
|
wim_name = wim_name ? wim_name + 1 : wim_path;
|
|
log_msg(L"Found %s", wim_name);
|
|
}
|
|
|
|
/* ---- Step 2.5: Detect image language ---- */
|
|
{
|
|
wchar_t dism_cmd[1024];
|
|
char dism_output[8192];
|
|
wchar_t sys_dir[MAX_PATH];
|
|
const char *detected_lang = "en-US"; /* fallback */
|
|
GetSystemDirectoryW(sys_dir, MAX_PATH);
|
|
swprintf_s(dism_cmd, 1024,
|
|
L"%s\\dism.exe /Get-WimInfo /WimFile:\"%s\" /Index:%d",
|
|
sys_dir, wim_path, image_index);
|
|
if (run_command_capture(dism_cmd, dism_output, sizeof(dism_output)) == 0) {
|
|
/* Look for "xx-YY (Default)" pattern in DISM output */
|
|
char *p = strstr(dism_output, "(Default)");
|
|
if (p) {
|
|
/* Walk backwards to find the language tag */
|
|
char *end = p;
|
|
while (end > dism_output && *(end - 1) == ' ') end--;
|
|
if (end > dism_output) {
|
|
char *start = end;
|
|
while (start > dism_output && *(start - 1) != ' ' &&
|
|
*(start - 1) != '\n' && *(start - 1) != '\r' &&
|
|
*(start - 1) != '\t')
|
|
start--;
|
|
if (start < end) {
|
|
*end = '\0';
|
|
detected_lang = start;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
printf("LANG:%s\n", detected_lang);
|
|
fflush(stdout);
|
|
}
|
|
|
|
/* ---- Step 3: Create VHDX ---- */
|
|
log_msg(L"Creating %d GB VHDX...", size_gb);
|
|
{
|
|
VIRTUAL_STORAGE_TYPE st;
|
|
CREATE_VIRTUAL_DISK_PARAMETERS params;
|
|
|
|
st.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
|
|
st.VendorId = VHDX_VENDOR_MS;
|
|
|
|
ZeroMemory(¶ms, sizeof(params));
|
|
params.Version = CREATE_VIRTUAL_DISK_VERSION_2;
|
|
params.Version2.MaximumSize = (ULONGLONG)size_gb * 1024ULL * 1024ULL * 1024ULL;
|
|
params.Version2.BlockSizeInBytes = 0;
|
|
params.Version2.SectorSizeInBytes = 512;
|
|
params.Version2.PhysicalSectorSizeInBytes = 4096;
|
|
|
|
result = CreateVirtualDisk(&st, vhdx_path,
|
|
VIRTUAL_DISK_ACCESS_NONE,
|
|
NULL,
|
|
CREATE_VIRTUAL_DISK_FLAG_NONE,
|
|
0, ¶ms, NULL, &vhdx_handle);
|
|
if (result != ERROR_SUCCESS) {
|
|
log_err(L"CreateVirtualDisk failed (error %lu)", result);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
/* ---- Step 4: Attach VHDX ---- */
|
|
{
|
|
ATTACH_VIRTUAL_DISK_PARAMETERS ap;
|
|
ZeroMemory(&ap, sizeof(ap));
|
|
ap.Version = ATTACH_VIRTUAL_DISK_VERSION_1;
|
|
|
|
result = AttachVirtualDisk(vhdx_handle, NULL,
|
|
ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER,
|
|
0, &ap, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
log_err(L"AttachVirtualDisk(VHDX) failed (error %lu)", result);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* Get physical path */
|
|
{
|
|
DWORD phys_len = sizeof(phys_path);
|
|
result = GetVirtualDiskPhysicalPath(vhdx_handle, &phys_len, phys_path);
|
|
if (result != ERROR_SUCCESS) {
|
|
log_err(L"GetVirtualDiskPhysicalPath failed (error %lu)", result);
|
|
goto cleanup;
|
|
}
|
|
disk_number = get_disk_number_from_path(phys_path);
|
|
}
|
|
|
|
/* Open the physical disk */
|
|
disk_handle = CreateFileW(phys_path, GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
NULL, OPEN_EXISTING, 0, NULL);
|
|
if (disk_handle == INVALID_HANDLE_VALUE) {
|
|
log_err(L"Failed to open disk %s (error %lu)", phys_path, GetLastError());
|
|
goto cleanup;
|
|
}
|
|
|
|
/* ---- Step 5: Initialize GPT and create partitions ---- */
|
|
log_msg(L"Partitioning disk...");
|
|
{
|
|
CREATE_DISK cd;
|
|
DWORD bytes;
|
|
|
|
ZeroMemory(&cd, sizeof(cd));
|
|
cd.PartitionStyle = PARTITION_STYLE_GPT;
|
|
/* Let Windows generate a disk GUID */
|
|
CoCreateGuid(&cd.Gpt.DiskId);
|
|
|
|
if (!DeviceIoControl(disk_handle, IOCTL_DISK_CREATE_DISK,
|
|
&cd, sizeof(cd), NULL, 0, &bytes, NULL)) {
|
|
log_err(L"IOCTL_DISK_CREATE_DISK failed (error %lu)", GetLastError());
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* Refresh partition table */
|
|
{
|
|
DWORD bytes;
|
|
DeviceIoControl(disk_handle, IOCTL_DISK_UPDATE_PROPERTIES,
|
|
NULL, 0, NULL, 0, &bytes, NULL);
|
|
}
|
|
|
|
/* Create EFI, MSR, and Windows partitions */
|
|
{
|
|
DWORD bytes;
|
|
DWORD layout_size;
|
|
DRIVE_LAYOUT_INFORMATION_EX *layout;
|
|
PARTITION_INFORMATION_EX *p;
|
|
LARGE_INTEGER efi_size, msr_size;
|
|
LARGE_INTEGER efi_offset;
|
|
GET_LENGTH_INFORMATION leninfo;
|
|
|
|
/* Get total disk size */
|
|
if (!DeviceIoControl(disk_handle, IOCTL_DISK_GET_LENGTH_INFO,
|
|
NULL, 0, &leninfo, sizeof(leninfo), &bytes, NULL)) {
|
|
log_err(L"IOCTL_DISK_GET_LENGTH_INFO failed (error %lu)", GetLastError());
|
|
goto cleanup;
|
|
}
|
|
efi_size.QuadPart = (LONGLONG)EFI_SIZE_MB * 1024LL * 1024LL;
|
|
msr_size.QuadPart = (LONGLONG)MSR_SIZE_MB * 1024LL * 1024LL;
|
|
efi_offset.QuadPart = 1024LL * 1024LL; /* 1MB offset (standard GPT alignment) */
|
|
|
|
/* Allocate layout buffer for 3 partitions */
|
|
layout_size = (DWORD)(sizeof(DRIVE_LAYOUT_INFORMATION_EX) +
|
|
3 * sizeof(PARTITION_INFORMATION_EX));
|
|
layout = (DRIVE_LAYOUT_INFORMATION_EX *)calloc(1, layout_size);
|
|
if (!layout) {
|
|
log_err(L"Out of memory");
|
|
goto cleanup;
|
|
}
|
|
|
|
layout->PartitionStyle = PARTITION_STYLE_GPT;
|
|
layout->PartitionCount = 3;
|
|
CoCreateGuid(&layout->Gpt.DiskId);
|
|
|
|
/* Partition 1: EFI System Partition */
|
|
p = &layout->PartitionEntry[0];
|
|
p->PartitionStyle = PARTITION_STYLE_GPT;
|
|
p->StartingOffset.QuadPart = efi_offset.QuadPart;
|
|
p->PartitionLength.QuadPart = efi_size.QuadPart;
|
|
p->PartitionNumber = 1;
|
|
/* PARTITION_SYSTEM_GUID = {c12a7328-f81f-11d2-ba4b-00a0c93ec93b} */
|
|
p->Gpt.PartitionType.Data1 = 0xc12a7328;
|
|
p->Gpt.PartitionType.Data2 = 0xf81f;
|
|
p->Gpt.PartitionType.Data3 = 0x11d2;
|
|
p->Gpt.PartitionType.Data4[0] = 0xba; p->Gpt.PartitionType.Data4[1] = 0x4b;
|
|
p->Gpt.PartitionType.Data4[2] = 0x00; p->Gpt.PartitionType.Data4[3] = 0xa0;
|
|
p->Gpt.PartitionType.Data4[4] = 0xc9; p->Gpt.PartitionType.Data4[5] = 0x3e;
|
|
p->Gpt.PartitionType.Data4[6] = 0xc9; p->Gpt.PartitionType.Data4[7] = 0x3b;
|
|
CoCreateGuid(&p->Gpt.PartitionId);
|
|
wcscpy_s(p->Gpt.Name, 36, L"EFI System Partition");
|
|
|
|
/* Partition 2: Microsoft Reserved */
|
|
p = &layout->PartitionEntry[1];
|
|
p->PartitionStyle = PARTITION_STYLE_GPT;
|
|
p->StartingOffset.QuadPart = efi_offset.QuadPart + efi_size.QuadPart;
|
|
p->PartitionLength.QuadPart = msr_size.QuadPart;
|
|
p->PartitionNumber = 2;
|
|
/* PARTITION_MSFT_RESERVED_GUID = {e3c9e316-0b5c-4db8-817d-f92df00215ae} */
|
|
p->Gpt.PartitionType.Data1 = 0xe3c9e316;
|
|
p->Gpt.PartitionType.Data2 = 0x0b5c;
|
|
p->Gpt.PartitionType.Data3 = 0x4db8;
|
|
p->Gpt.PartitionType.Data4[0] = 0x81; p->Gpt.PartitionType.Data4[1] = 0x7d;
|
|
p->Gpt.PartitionType.Data4[2] = 0xf9; p->Gpt.PartitionType.Data4[3] = 0x2d;
|
|
p->Gpt.PartitionType.Data4[4] = 0xf0; p->Gpt.PartitionType.Data4[5] = 0x02;
|
|
p->Gpt.PartitionType.Data4[6] = 0x15; p->Gpt.PartitionType.Data4[7] = 0xae;
|
|
CoCreateGuid(&p->Gpt.PartitionId);
|
|
|
|
/* Partition 3: Windows (Basic Data) — uses remaining space minus 1MB at end */
|
|
p = &layout->PartitionEntry[2];
|
|
p->PartitionStyle = PARTITION_STYLE_GPT;
|
|
p->StartingOffset.QuadPart = efi_offset.QuadPart + efi_size.QuadPart + msr_size.QuadPart;
|
|
p->PartitionLength.QuadPart = leninfo.Length.QuadPart - p->StartingOffset.QuadPart - 1024LL * 1024LL;
|
|
p->PartitionNumber = 3;
|
|
/* PARTITION_BASIC_DATA_GUID = {ebd0a0a2-b9e5-4433-87c0-68b6b72699c7} */
|
|
p->Gpt.PartitionType.Data1 = 0xebd0a0a2;
|
|
p->Gpt.PartitionType.Data2 = 0xb9e5;
|
|
p->Gpt.PartitionType.Data3 = 0x4433;
|
|
p->Gpt.PartitionType.Data4[0] = 0x87; p->Gpt.PartitionType.Data4[1] = 0xc0;
|
|
p->Gpt.PartitionType.Data4[2] = 0x68; p->Gpt.PartitionType.Data4[3] = 0xb6;
|
|
p->Gpt.PartitionType.Data4[4] = 0xb7; p->Gpt.PartitionType.Data4[5] = 0x26;
|
|
p->Gpt.PartitionType.Data4[6] = 0x99; p->Gpt.PartitionType.Data4[7] = 0xc7;
|
|
CoCreateGuid(&p->Gpt.PartitionId);
|
|
|
|
if (!DeviceIoControl(disk_handle, IOCTL_DISK_SET_DRIVE_LAYOUT_EX,
|
|
layout, layout_size, NULL, 0, &bytes, NULL)) {
|
|
log_err(L"IOCTL_DISK_SET_DRIVE_LAYOUT_EX failed (error %lu)", GetLastError());
|
|
free(layout);
|
|
goto cleanup;
|
|
}
|
|
free(layout);
|
|
}
|
|
|
|
/* Refresh again after partitioning */
|
|
{
|
|
DWORD bytes;
|
|
DeviceIoControl(disk_handle, IOCTL_DISK_UPDATE_PROPERTIES,
|
|
NULL, 0, NULL, 0, &bytes, NULL);
|
|
}
|
|
|
|
/* Give the OS time to enumerate the new volumes */
|
|
Sleep(2000);
|
|
|
|
/* ---- Step 6: Find and mount the EFI and Windows volumes ---- */
|
|
log_msg(L"Formatting partitions...");
|
|
{
|
|
wchar_t efi_vol[MAX_PATH];
|
|
if (!find_volume_for_partition(disk_number, 1, efi_vol, MAX_PATH)) {
|
|
log_err(L"Could not find EFI partition volume");
|
|
goto cleanup;
|
|
}
|
|
if (!create_temp_mount_dir(L"asb_efi", efi_mount, MAX_PATH)) {
|
|
log_err(L"Failed to create temp mount directory");
|
|
goto cleanup;
|
|
}
|
|
if (!mount_volume_to_dir(efi_vol, efi_mount)) {
|
|
log_err(L"Failed to mount EFI partition (error %lu)", GetLastError());
|
|
goto cleanup;
|
|
}
|
|
efi_mounted = TRUE;
|
|
}
|
|
|
|
{
|
|
wchar_t win_vol[MAX_PATH];
|
|
if (!find_volume_for_partition(disk_number, 3, win_vol, MAX_PATH)) {
|
|
log_err(L"Could not find Windows partition volume");
|
|
goto cleanup;
|
|
}
|
|
if (!create_temp_mount_dir(L"asb_win", win_mount, MAX_PATH)) {
|
|
log_err(L"Failed to create temp mount directory");
|
|
goto cleanup;
|
|
}
|
|
if (!mount_volume_to_dir(win_vol, win_mount)) {
|
|
log_err(L"Failed to mount Windows partition (error %lu)", GetLastError());
|
|
goto cleanup;
|
|
}
|
|
win_mounted = TRUE;
|
|
}
|
|
|
|
/* ---- Step 7: Format partitions ---- */
|
|
if (!format_volume(efi_mount, L"FAT32", L"SYSTEM")) {
|
|
log_err(L"Failed to format EFI partition");
|
|
goto cleanup;
|
|
}
|
|
if (!format_volume(win_mount, L"NTFS", L"Windows")) {
|
|
log_err(L"Failed to format Windows partition");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* ---- Step 8: Apply WIM/ESD image using dism.exe ---- */
|
|
{
|
|
wchar_t cmd[1024];
|
|
wchar_t sys_dir[MAX_PATH];
|
|
wchar_t win_clean[MAX_PATH];
|
|
int ret;
|
|
|
|
GetSystemDirectoryW(sys_dir, MAX_PATH);
|
|
|
|
/* Strip trailing backslash from win_mount for dism */
|
|
wcscpy_s(win_clean, MAX_PATH, win_mount);
|
|
{
|
|
size_t wl = wcslen(win_clean);
|
|
if (wl > 0 && win_clean[wl - 1] == L'\\')
|
|
win_clean[wl - 1] = L'\0';
|
|
}
|
|
|
|
swprintf_s(cmd, 1024,
|
|
L"%s\\dism.exe /Apply-Image /ImageFile:\"%s\" /Index:%d /ApplyDir:\"%s\"",
|
|
sys_dir, wim_path, image_index, win_clean);
|
|
|
|
ret = run_command_with_progress(cmd, L"Applying Windows image...");
|
|
if (ret != 0) {
|
|
log_err(L"Failed to apply Windows image (dism exit code %d)", ret);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* ---- Step 9: Stage additional files from manifest ---- */
|
|
if (manifest_path) {
|
|
log_msg(L"Staging files...");
|
|
{
|
|
int staged = stage_files(manifest_path, win_mount);
|
|
if (staged < 0) {
|
|
log_err(L"File staging failed");
|
|
goto cleanup;
|
|
}
|
|
if (staged > 0)
|
|
log_msg(L"Staged %d file(s)", staged);
|
|
}
|
|
}
|
|
|
|
/* ---- Step 10: Set up UEFI boot files with bcdboot ---- */
|
|
log_msg(L"Installing boot files...");
|
|
{
|
|
wchar_t windows_dir[MAX_PATH];
|
|
wchar_t efi_clean[MAX_PATH];
|
|
wchar_t cmd[1024];
|
|
wchar_t sys_dir[MAX_PATH];
|
|
int ret;
|
|
|
|
GetSystemDirectoryW(sys_dir, MAX_PATH);
|
|
swprintf_s(windows_dir, MAX_PATH, L"%sWindows", win_mount);
|
|
|
|
/* Strip trailing backslash from efi_mount for bcdboot */
|
|
wcscpy_s(efi_clean, MAX_PATH, efi_mount);
|
|
{
|
|
size_t el = wcslen(efi_clean);
|
|
if (el > 0 && efi_clean[el - 1] == L'\\')
|
|
efi_clean[el - 1] = L'\0';
|
|
}
|
|
|
|
swprintf_s(cmd, 1024,
|
|
L"%s\\bcdboot.exe \"%s\" /s \"%s\" /f UEFI",
|
|
sys_dir, windows_dir, efi_clean);
|
|
|
|
ret = run_command(cmd, TRUE);
|
|
if (ret != 0) {
|
|
log_err(L"Failed to install boot files (bcdboot exit code %d)", ret);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
log_done(vhdx_path);
|
|
exit_code = 0;
|
|
|
|
cleanup:
|
|
/* Unmount volumes */
|
|
if (win_mounted) {
|
|
unmount_volume_from_dir(win_mount);
|
|
RemoveDirectoryW(win_mount);
|
|
}
|
|
if (efi_mounted) {
|
|
unmount_volume_from_dir(efi_mount);
|
|
RemoveDirectoryW(efi_mount);
|
|
}
|
|
/* Close disk handle before detaching */
|
|
if (disk_handle != INVALID_HANDLE_VALUE)
|
|
CloseHandle(disk_handle);
|
|
|
|
/* Detach and close VHDX */
|
|
if (vhdx_handle != INVALID_HANDLE_VALUE) {
|
|
DetachVirtualDisk(vhdx_handle, DETACH_VIRTUAL_DISK_FLAG_NONE, 0);
|
|
CloseHandle(vhdx_handle);
|
|
}
|
|
|
|
/* Detach and close ISO */
|
|
if (iso_handle != INVALID_HANDLE_VALUE) {
|
|
DetachVirtualDisk(iso_handle, DETACH_VIRTUAL_DISK_FLAG_NONE, 0);
|
|
CloseHandle(iso_handle);
|
|
}
|
|
|
|
/* If we failed, delete the partial VHDX */
|
|
if (exit_code != 0 && vhdx_path[0]) {
|
|
DeleteFileW(vhdx_path);
|
|
}
|
|
|
|
return exit_code;
|
|
}
|
|
|
|
/* ======================================================================
|
|
* Noprompt ISO rebuild (original functionality)
|
|
* ====================================================================== */
|
|
|
|
static int do_noprompt(const wchar_t *iso_path_arg)
|
|
{
|
|
HRESULT hr;
|
|
DWORD result;
|
|
int exit_code = 1;
|
|
|
|
/* State that needs cleanup */
|
|
HANDLE iso_handle = INVALID_HANDLE_VALUE;
|
|
wchar_t iso_drive = 0;
|
|
DWORD cdrom_before = 0;
|
|
IFileSystemImage *pImage = NULL;
|
|
IFsiDirectoryItem *pRoot = NULL;
|
|
IFsiDirectoryItem *pBootDir = NULL;
|
|
IBootOptions *pBootOpts = NULL;
|
|
IStream *pBootStream = NULL;
|
|
IStream *pCdbootStream = NULL;
|
|
IFileSystemImageResult *pResult = NULL;
|
|
IStream *pOutStream = NULL;
|
|
|
|
/* Paths */
|
|
wchar_t iso_path[MAX_PATH];
|
|
wchar_t output_path[MAX_PATH];
|
|
wchar_t drive_root[4];
|
|
wchar_t noprompt_efisys[MAX_PATH];
|
|
wchar_t noprompt_cdboot[MAX_PATH];
|
|
wchar_t vol_label[MAX_PATH];
|
|
|
|
/* Resolve full path */
|
|
if (!GetFullPathNameW(iso_path_arg, MAX_PATH, iso_path, NULL)) {
|
|
log_err(L"Invalid path: %s", iso_path_arg);
|
|
return 1;
|
|
}
|
|
if (GetFileAttributesW(iso_path) == INVALID_FILE_ATTRIBUTES) {
|
|
log_err(L"File not found: %s", iso_path);
|
|
return 1;
|
|
}
|
|
|
|
build_output_path(output_path, MAX_PATH, iso_path, L"_noprompt.iso");
|
|
|
|
/* ---- Step 1: Initialize COM ---- */
|
|
hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
|
if (FAILED(hr)) {
|
|
log_err(L"CoInitializeEx failed (0x%08X)", hr);
|
|
return 1;
|
|
}
|
|
|
|
/* ---- Step 2: Mount the source ISO ---- */
|
|
log_msg(L"Mounting %s...", iso_path);
|
|
{
|
|
VIRTUAL_STORAGE_TYPE st;
|
|
OPEN_VIRTUAL_DISK_PARAMETERS op;
|
|
|
|
ZeroMemory(&st, sizeof(st));
|
|
st.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_ISO;
|
|
|
|
ZeroMemory(&op, sizeof(op));
|
|
op.Version = OPEN_VIRTUAL_DISK_VERSION_1;
|
|
|
|
cdrom_before = snapshot_cdrom_drives();
|
|
log_msg(L"CDROM drives before mount: 0x%08X", cdrom_before);
|
|
|
|
result = OpenVirtualDisk(&st, iso_path,
|
|
VIRTUAL_DISK_ACCESS_READ,
|
|
OPEN_VIRTUAL_DISK_FLAG_NONE,
|
|
&op, &iso_handle);
|
|
if (result != ERROR_SUCCESS) {
|
|
log_err(L"OpenVirtualDisk failed (error %lu)", result);
|
|
goto cleanup;
|
|
}
|
|
log_msg(L"OpenVirtualDisk succeeded");
|
|
|
|
result = AttachVirtualDisk(iso_handle, NULL,
|
|
ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY,
|
|
0, NULL, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
log_err(L"AttachVirtualDisk failed (error %lu)", result);
|
|
goto cleanup;
|
|
}
|
|
log_msg(L"AttachVirtualDisk succeeded");
|
|
|
|
/* Get the physical path assigned by the system */
|
|
{
|
|
wchar_t phys_path[MAX_PATH];
|
|
DWORD phys_len = sizeof(phys_path);
|
|
result = GetVirtualDiskPhysicalPath(iso_handle, &phys_len, phys_path);
|
|
if (result == ERROR_SUCCESS)
|
|
log_msg(L"Physical path: %s", phys_path);
|
|
else
|
|
log_msg(L"GetVirtualDiskPhysicalPath failed (error %lu)", result);
|
|
}
|
|
|
|
/* Wait for the drive letter to appear */
|
|
for (int i = 0; i < 20; i++) {
|
|
DWORD cdrom_now = snapshot_cdrom_drives();
|
|
DWORD new_drives = cdrom_now & ~cdrom_before;
|
|
if (i == 0 || (i % 4 == 0))
|
|
log_msg(L"Poll %d: CDROM drives now: 0x%08X, new: 0x%08X", i, cdrom_now, new_drives);
|
|
if (new_drives) {
|
|
for (int b = 0; b < 26; b++) {
|
|
if (new_drives & (1u << b)) {
|
|
iso_drive = (wchar_t)(L'A' + b);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
Sleep(500);
|
|
}
|
|
if (!iso_drive) {
|
|
wchar_t root[4] = L"A:\\";
|
|
for (int i = 0; i < 26; i++) {
|
|
root[0] = (wchar_t)(L'A' + i);
|
|
UINT dt = GetDriveTypeW(root);
|
|
if (dt != DRIVE_NO_ROOT_DIR)
|
|
log_msg(L" %c: type=%u", (wchar_t)(L'A' + i), dt);
|
|
}
|
|
log_err(L"Mounted ISO but no new drive letter appeared");
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
swprintf_s(drive_root, 4, L"%c:\\", iso_drive);
|
|
log_msg(L"Mounted at %c:\\", iso_drive);
|
|
|
|
/* Wait for the filesystem to become accessible */
|
|
{
|
|
int ready = 0;
|
|
wchar_t test_label[64];
|
|
for (int i = 0; i < 30; i++) {
|
|
if (GetVolumeInformationW(drive_root, test_label, 64, NULL, NULL, NULL, NULL, 0)) {
|
|
ready = 1;
|
|
break;
|
|
}
|
|
log_msg(L"Waiting for volume to be ready... (attempt %d, error %lu)", i + 1, GetLastError());
|
|
Sleep(500);
|
|
}
|
|
if (!ready) {
|
|
log_err(L"Volume %c:\\ never became accessible", iso_drive);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* ---- Step 3: Verify noprompt files ---- */
|
|
swprintf_s(noprompt_efisys, MAX_PATH, L"%c:\\efi\\microsoft\\boot\\efisys_noprompt.bin", iso_drive);
|
|
swprintf_s(noprompt_cdboot, MAX_PATH, L"%c:\\efi\\microsoft\\boot\\cdboot_noprompt.efi", iso_drive);
|
|
|
|
log_msg(L"Checking: %s", noprompt_efisys);
|
|
if (GetFileAttributesW(noprompt_efisys) == INVALID_FILE_ATTRIBUTES) {
|
|
log_err(L"efisys_noprompt.bin not found on ISO (error %lu)", GetLastError());
|
|
goto cleanup;
|
|
}
|
|
log_msg(L"Checking: %s", noprompt_cdboot);
|
|
if (GetFileAttributesW(noprompt_cdboot) == INVALID_FILE_ATTRIBUTES) {
|
|
log_err(L"cdboot_noprompt.efi not found on ISO (error %lu)", GetLastError());
|
|
goto cleanup;
|
|
}
|
|
log_msg(L"Found efisys_noprompt.bin and cdboot_noprompt.efi");
|
|
|
|
/* ---- Step 4: Read volume label ---- */
|
|
vol_label[0] = L'\0';
|
|
GetVolumeInformationW(drive_root, vol_label, MAX_PATH, NULL, NULL, NULL, NULL, 0);
|
|
if (vol_label[0])
|
|
log_msg(L"Volume label: %s", vol_label);
|
|
|
|
/* ---- Step 5: Create IMAPI2 IFileSystemImage ---- */
|
|
hr = CoCreateInstance(&CLSID_FSImage, NULL, CLSCTX_ALL,
|
|
&IID_IFSImage, (void **)&pImage);
|
|
if (FAILED(hr)) {
|
|
log_err(L"CoCreateInstance(MsftFileSystemImage) failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
pImage->lpVtbl->put_FileSystemsToCreate(pImage, FsiFileSystemUDF);
|
|
pImage->lpVtbl->put_UDFRevision(pImage, 0x0150);
|
|
pImage->lpVtbl->put_FreeMediaBlocks(pImage, 4194304);
|
|
|
|
if (vol_label[0]) {
|
|
BSTR bstrLabel = SysAllocString(vol_label);
|
|
pImage->lpVtbl->put_VolumeName(pImage, bstrLabel);
|
|
SysFreeString(bstrLabel);
|
|
}
|
|
|
|
/* ---- Step 6: Set UEFI boot image ---- */
|
|
hr = SHCreateStreamOnFileW(noprompt_efisys, STGM_READ | STGM_SHARE_DENY_WRITE,
|
|
&pBootStream);
|
|
if (FAILED(hr)) {
|
|
log_err(L"SHCreateStreamOnFileW(efisys_noprompt.bin) failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
hr = CoCreateInstance(&CLSID_BootOpts, NULL, CLSCTX_ALL,
|
|
&IID_IBootOpts, (void **)&pBootOpts);
|
|
if (FAILED(hr)) {
|
|
log_err(L"CoCreateInstance(BootOptions) failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
pBootOpts->lpVtbl->AssignBootImage(pBootOpts, pBootStream);
|
|
pBootOpts->lpVtbl->put_PlatformId(pBootOpts, 0xEF);
|
|
pBootOpts->lpVtbl->put_Emulation(pBootOpts, 0);
|
|
|
|
hr = pImage->lpVtbl->put_BootImageOptions(pImage, pBootOpts);
|
|
if (FAILED(hr)) {
|
|
log_err(L"put_BootImageOptions failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
/* ---- Step 7: Import source ISO file tree ---- */
|
|
log_msg(L"Importing files from source ISO...");
|
|
hr = pImage->lpVtbl->get_Root(pImage, &pRoot);
|
|
if (FAILED(hr)) {
|
|
log_err(L"get_Root failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
{
|
|
BSTR bstrDrive = SysAllocString(drive_root);
|
|
hr = pRoot->lpVtbl->AddTree(pRoot, bstrDrive, VARIANT_FALSE);
|
|
SysFreeString(bstrDrive);
|
|
}
|
|
if (FAILED(hr)) {
|
|
log_err(L"AddTree failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
/* ---- Step 8: Swap boot files ---- */
|
|
log_msg(L"Swapping boot files with noprompt variants...");
|
|
hr = navigate_fsi_path(pRoot, &pBootDir,
|
|
L"efi", L"microsoft", L"boot", NULL);
|
|
if (FAILED(hr)) {
|
|
log_err(L"Failed to navigate to \\efi\\microsoft\\boot\\ (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
{
|
|
BSTR name;
|
|
name = SysAllocString(L"efisys.bin");
|
|
hr = pBootDir->lpVtbl->Remove(pBootDir, name);
|
|
SysFreeString(name);
|
|
if (FAILED(hr)) {
|
|
log_err(L"Failed to remove efisys.bin from tree (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
name = SysAllocString(L"cdboot.efi");
|
|
hr = pBootDir->lpVtbl->Remove(pBootDir, name);
|
|
SysFreeString(name);
|
|
if (FAILED(hr)) {
|
|
log_err(L"Failed to remove cdboot.efi from tree (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
{
|
|
BSTR name;
|
|
IStream *stream = NULL;
|
|
|
|
hr = SHCreateStreamOnFileW(noprompt_efisys, STGM_READ | STGM_SHARE_DENY_WRITE, &stream);
|
|
if (FAILED(hr)) {
|
|
log_err(L"SHCreateStreamOnFileW(efisys_noprompt) failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
name = SysAllocString(L"efisys.bin");
|
|
hr = pBootDir->lpVtbl->AddFile(pBootDir, name, stream);
|
|
SysFreeString(name);
|
|
stream->lpVtbl->Release(stream);
|
|
stream = NULL;
|
|
if (FAILED(hr)) {
|
|
log_err(L"AddFile(efisys.bin) failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
hr = SHCreateStreamOnFileW(noprompt_cdboot, STGM_READ | STGM_SHARE_DENY_WRITE, &stream);
|
|
if (FAILED(hr)) {
|
|
log_err(L"SHCreateStreamOnFileW(cdboot_noprompt) failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
name = SysAllocString(L"cdboot.efi");
|
|
hr = pBootDir->lpVtbl->AddFile(pBootDir, name, stream);
|
|
SysFreeString(name);
|
|
stream->lpVtbl->Release(stream);
|
|
stream = NULL;
|
|
if (FAILED(hr)) {
|
|
log_err(L"AddFile(cdboot.efi) failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/* ---- Step 9: Build and write the patched ISO ---- */
|
|
log_msg(L"Building patched ISO (this may take several minutes)...");
|
|
hr = pImage->lpVtbl->CreateResultImage(pImage, &pResult);
|
|
if (FAILED(hr)) {
|
|
log_err(L"CreateResultImage failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
hr = pResult->lpVtbl->get_ImageStream(pResult, &pOutStream);
|
|
if (FAILED(hr)) {
|
|
log_err(L"get_ImageStream failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
log_msg(L"Writing %s...", output_path);
|
|
hr = write_stream_to_file(pOutStream, output_path);
|
|
if (FAILED(hr)) {
|
|
log_err(L"write_stream_to_file failed (0x%08X)", hr);
|
|
goto cleanup;
|
|
}
|
|
|
|
log_msg(L"Done.");
|
|
exit_code = 0;
|
|
|
|
cleanup:
|
|
if (pOutStream) pOutStream->lpVtbl->Release(pOutStream);
|
|
if (pResult) pResult->lpVtbl->Release(pResult);
|
|
if (pCdbootStream) pCdbootStream->lpVtbl->Release(pCdbootStream);
|
|
if (pBootStream) pBootStream->lpVtbl->Release(pBootStream);
|
|
if (pBootOpts) pBootOpts->lpVtbl->Release(pBootOpts);
|
|
if (pBootDir) pBootDir->lpVtbl->Release(pBootDir);
|
|
if (pRoot) pRoot->lpVtbl->Release(pRoot);
|
|
if (pImage) pImage->lpVtbl->Release(pImage);
|
|
|
|
if (iso_handle != INVALID_HANDLE_VALUE) {
|
|
DetachVirtualDisk(iso_handle, DETACH_VIRTUAL_DISK_FLAG_NONE, 0);
|
|
CloseHandle(iso_handle);
|
|
}
|
|
|
|
CoUninitialize();
|
|
return exit_code;
|
|
}
|
|
|
|
/* ---- Main ---- */
|
|
|
|
int wmain(int argc, wchar_t *argv[])
|
|
{
|
|
if (argc < 2) {
|
|
wprintf(L"Usage:\n");
|
|
wprintf(L" iso-patch.exe <windows.iso>\n");
|
|
wprintf(L" Rebuild ISO with noprompt UEFI boot (no \"Press Any Key\").\n\n");
|
|
wprintf(L" iso-patch.exe --to-vhdx <windows.iso> [image-index] [size-gb] [options]\n");
|
|
wprintf(L" Convert Windows installer ISO to a bootable UEFI VHDX.\n");
|
|
wprintf(L" image-index defaults to 1 (first edition in install.wim/esd).\n");
|
|
wprintf(L" size-gb defaults to %d.\n", DEFAULT_VHDX_SIZE_GB);
|
|
wprintf(L" --output <path> Output VHDX path (default: next to iso-patch.exe).\n");
|
|
wprintf(L" --stage <file> Tab-separated manifest of files to copy onto the VHDX.\n");
|
|
wprintf(L" Each line: <source_path>\\t<dest_relative_to_root>\n\n");
|
|
wprintf(L"Output defaults to next to iso-patch.exe.\n");
|
|
return 1;
|
|
}
|
|
|
|
if (_wcsicmp(argv[1], L"--to-vhdx") == 0) {
|
|
int image_index = 1;
|
|
int size_gb = DEFAULT_VHDX_SIZE_GB;
|
|
const wchar_t *manifest = NULL;
|
|
const wchar_t *output = NULL;
|
|
int positional = 0;
|
|
if (argc < 3) {
|
|
log_err(L"--to-vhdx requires an ISO path");
|
|
return 1;
|
|
}
|
|
/* Parse optional positional args and flags */
|
|
for (int i = 3; i < argc; i++) {
|
|
if (_wcsicmp(argv[i], L"--stage") == 0) {
|
|
if (i + 1 < argc) { manifest = argv[++i]; }
|
|
else { log_err(L"--stage requires a manifest file path"); return 1; }
|
|
} else if (_wcsicmp(argv[i], L"--output") == 0) {
|
|
if (i + 1 < argc) { output = argv[++i]; }
|
|
else { log_err(L"--output requires a file path"); return 1; }
|
|
} else if (positional == 0) {
|
|
image_index = _wtoi(argv[i]);
|
|
if (image_index < 1) image_index = 1;
|
|
positional++;
|
|
} else if (positional == 1) {
|
|
size_gb = _wtoi(argv[i]);
|
|
if (size_gb < 16) size_gb = 16;
|
|
positional++;
|
|
}
|
|
}
|
|
return do_to_vhdx(argv[2], image_index, size_gb, manifest, output);
|
|
}
|
|
|
|
return do_noprompt(argv[1]);
|
|
}
|