Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/app_win/ui.c
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ static void on_webview2_message(const wchar_t *json)
wchar_t name_buf[256] = {0}, os_buf[32] = {0}, img_buf[MAX_PATH] = {0};
wchar_t tpl_buf[256] = {0}, user_buf[128] = {0}, pass_buf[128] = {0};
wchar_t adapter_buf[256] = {0};
wchar_t storage_buf[MAX_PATH] = {0};
int val;
BOOL is_tpl = FALSE;

Expand All @@ -902,6 +903,7 @@ static void on_webview2_message(const wchar_t *json)
json_get_string(json, L"adminUser", user_buf, 128);
json_get_string(json, L"adminPass", pass_buf, 128);
json_get_string(json, L"netAdapter", adapter_buf, 256);
json_get_string(json, L"storageFolder", storage_buf, MAX_PATH);
json_get_bool(json, L"isTemplate", &is_tpl);

ZeroMemory(&cfg, sizeof(cfg));
Expand All @@ -912,6 +914,7 @@ static void on_webview2_message(const wchar_t *json)
cfg.username = user_buf;
cfg.password = pass_buf;
cfg.net_adapter = adapter_buf;
cfg.storage_folder = storage_buf;
cfg.is_template = is_tpl;

if (json_get_int(json, L"hddGb", &val)) cfg.hdd_gb = (DWORD)val;
Expand Down Expand Up @@ -1074,6 +1077,67 @@ static void on_webview2_message(const wchar_t *json)
jb_object_end(&jb);
webview2_post(json_buf);
}
} else if (wcscmp(action, L"browseStorageFolder") == 0) {
IFileOpenDialog *dlg = NULL;
if (SUCCEEDED(CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
&IID_IFileOpenDialog, (void **)&dlg))) {
DWORD opts = 0;
dlg->lpVtbl->GetOptions(dlg, &opts);
dlg->lpVtbl->SetOptions(dlg, opts | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
dlg->lpVtbl->SetTitle(dlg, L"Choose storage folder");
if (SUCCEEDED(dlg->lpVtbl->Show(dlg, g_hwnd_main))) {
IShellItem *item = NULL;
if (SUCCEEDED(dlg->lpVtbl->GetResult(dlg, &item))) {
PWSTR path = NULL;
if (SUCCEEDED(item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &path))) {
wchar_t json_buf[MAX_PATH + 128];
JsonBuilder jb;
jb_init(&jb, json_buf, _countof(json_buf));
jb_object_begin(&jb);
jb_string(&jb, L"type", L"storageFolderPicked");
jb_string(&jb, L"path", path);
jb_object_end(&jb);
webview2_post(json_buf);
CoTaskMemFree(path);
}
item->lpVtbl->Release(item);
}
}
dlg->lpVtbl->Release(dlg);
}
} else if (wcscmp(action, L"getDriveFreeSpace") == 0) {
wchar_t path_buf[MAX_PATH] = {0};
ULARGE_INTEGER free_bytes = { 0 };
wchar_t root[4] = {0};
wchar_t json_buf[MAX_PATH + 128];
JsonBuilder jb;

json_get_string(json, L"path", path_buf, MAX_PATH);
if (path_buf[0] == 0) return;

/* Derive drive root: e.g. "R:\sandboxes" -> "R:\" */
if (wcslen(path_buf) >= 2 && path_buf[1] == L':') {
root[0] = path_buf[0]; root[1] = L':'; root[2] = L'\\'; root[3] = 0;
} else {
return; /* not an absolute path; JS will catch this */
}

if (!GetDiskFreeSpaceExW(root, &free_bytes, NULL, NULL)) return;

jb_init(&jb, json_buf, _countof(json_buf));
jb_object_begin(&jb);
jb_string(&jb, L"type", L"driveFreeSpace");
jb_string(&jb, L"path", path_buf);
/* JsonBuilder has only jb_int (32-bit). Serialize uint64 as a decimal
string so JS can Number() it without precision loss (Number handles
integers up to 2^53 safely). */
{
wchar_t num[32];
_snwprintf_s(num, _countof(num), _TRUNCATE, L"%llu", free_bytes.QuadPart);
jb_string(&jb, L"freeBytes", num);
}
jb_object_end(&jb);
webview2_post(json_buf);
} else if (wcscmp(action, L"snapTake") == 0) {
int vi;
wchar_t sname[128] = {0};
Expand Down
85 changes: 85 additions & 0 deletions src/backend_win/asb_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -2599,6 +2599,79 @@ ASB_API void asb_detach(void)
g_initialized = FALSE;
}

/* Validate a user-supplied storage folder, and (as a side effect) create
the target sandbox folder if it doesn't exist. Returns S_OK if OK or
empty; E_INVALIDARG with an asb_log explanation on failure. Empty folder
is OK (caller falls through to the default ProgramData path). Creates
parent directories recursively via SHCreateDirectoryExW so multi-level
paths like R:\new\nested\sandboxes work in one call. The later
single-level CreateDirectoryW in asb_vm_create still runs but is then
a harmless redundant call (returns ERROR_ALREADY_EXISTS, ignored). */
static HRESULT validate_storage_folder(const wchar_t *folder, const wchar_t *vm_name)
{
wchar_t root[4];
wchar_t target_disk[MAX_PATH];
UINT drive_type;
DWORD attrs;
int sh_rc;

if (!folder || !folder[0]) return S_OK; /* empty = use default; nothing to validate */

/* 1. Absolute Windows path: <letter>:\... */
if (wcslen(folder) < 3 ||
!((folder[0] >= L'A' && folder[0] <= L'Z') || (folder[0] >= L'a' && folder[0] <= L'z')) ||
folder[1] != L':' || folder[2] != L'\\') {
asb_log(L"Error: Storage folder must be an absolute path like R:\\sandboxes (got: %s)", folder);
return E_INVALIDARG;
}

/* 2. Drive is a local fixed volume. */
root[0] = folder[0]; root[1] = L':'; root[2] = L'\\'; root[3] = L'\0';
drive_type = GetDriveTypeW(root);
if (drive_type != DRIVE_FIXED) {
asb_log(L"Error: Storage folder must be on a local fixed drive, not network/removable (%s, type=%u)",
root, drive_type);
return E_INVALIDARG;
}

/* 3. Combined path fits in MAX_PATH. The downstream construction is
<folder>\<name>\disk.vhdx — 11 fixed chars (separator + "\\disk.vhdx").
Reject early so swprintf_s never silently empties the buffer. */
if (wcslen(folder) + wcslen(vm_name) + 11 >= MAX_PATH) {
asb_log(L"Error: Storage folder path is too long; folder + sandbox name + \\disk.vhdx must fit in %d characters.",
MAX_PATH);
return E_INVALIDARG;
}

/* 4. The target sandbox folder is creatable (recursively). Use
SHCreateDirectoryExW so that something like R:\new\nested\path\<name>
gets all intermediate levels created in one call. Treat
ERROR_FILE_EXISTS / ERROR_ALREADY_EXISTS as success. */
{
wchar_t target_dir[MAX_PATH];
swprintf_s(target_dir, MAX_PATH, L"%s\\%s", folder, vm_name);
sh_rc = SHCreateDirectoryExW(NULL, target_dir, NULL);
if (sh_rc != ERROR_SUCCESS &&
sh_rc != ERROR_FILE_EXISTS &&
sh_rc != ERROR_ALREADY_EXISTS) {
asb_log(L"Error: Can't create storage folder %s (Win32 error %d)",
target_dir, sh_rc);
return E_INVALIDARG;
}
}

/* 5. No existing disk.vhdx at the target sandbox folder. */
swprintf_s(target_disk, MAX_PATH, L"%s\\%s\\disk.vhdx", folder, vm_name);
attrs = GetFileAttributesW(target_disk);
if (attrs != INVALID_FILE_ATTRIBUTES) {
asb_log(L"Error: A sandbox already exists at %s. Pick a different folder or sandbox name.",
target_disk);
return E_INVALIDARG;
}

return S_OK;
}

/* ---- VM Create ---- */

ASB_API HRESULT asb_vm_create(const AsbVmConfig *config)
Expand Down Expand Up @@ -2627,6 +2700,7 @@ ASB_API HRESULT asb_vm_create(const AsbVmConfig *config)
if (config->image_path) wcscpy_s(cfg.image_path, MAX_PATH, config->image_path);
if (config->username) wcscpy_s(cfg.admin_user, 128, config->username);
if (config->password) wcscpy_s(cfg.admin_pass, 128, config->password);
if (config->storage_folder) wcscpy_s(cfg.storage_folder, MAX_PATH, config->storage_folder);
cfg.ram_mb = config->ram_mb;
cfg.hdd_gb = config->hdd_gb;
cfg.cpu_cores = config->cpu_cores;
Expand All @@ -2644,6 +2718,12 @@ ASB_API HRESULT asb_vm_create(const AsbVmConfig *config)
is_template_create = config->is_template;
cfg.is_template = is_template_create;

/* Validate user-supplied storage folder (if any). Empty folder is OK. */
if (!is_template_create) {
HRESULT vhr = validate_storage_folder(cfg.storage_folder, cfg.name);
if (FAILED(vhr)) return vhr;
}

/* Defaults */
if (cfg.hdd_gb == 0) cfg.hdd_gb = 64;
if (cfg.ram_mb == 0) cfg.ram_mb = 4096;
Expand Down Expand Up @@ -2750,10 +2830,15 @@ ASB_API HRESULT asb_vm_create(const AsbVmConfig *config)
CreateDirectoryW(vhdx_dir, NULL);

if (is_template_create) {
/* Templates always live host-wide in ProgramData. */
swprintf_s(vhdx_dir, MAX_PATH, L"%s\\AppSandbox\\templates", base_dir);
CreateDirectoryW(vhdx_dir, NULL);
swprintf_s(vhdx_dir, MAX_PATH, L"%s\\AppSandbox\\templates\\%s", base_dir, cfg.name);
} else if (cfg.storage_folder[0]) {
/* User-chosen folder: <storage_folder>\<name>. */
swprintf_s(vhdx_dir, MAX_PATH, L"%s\\%s", cfg.storage_folder, cfg.name);
} else {
/* Default: %ProgramData%\AppSandbox\<name>. */
swprintf_s(vhdx_dir, MAX_PATH, L"%s\\AppSandbox\\%s", base_dir, cfg.name);
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/backend_win/asb_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ typedef struct {
BOOL ssh_enabled; /* TRUE = install OpenSSH Server in guest */
BOOL ssh_deploy_key; /* TRUE = deploy the AppSandbox public key (needs ssh_enabled) */
BOOL is_template; /* TRUE = create as template VM */
/* Optional: folder under which `<storage_folder>\<name>\disk.vhdx` will
live. NULL or empty = use default `%ProgramData%\AppSandbox\<name>\`.
Ignored for templates (which always live in ProgramData). Windows only. */
const wchar_t *storage_folder;
} AsbVmConfig;

/* ---- Snapshot/branch info (returned by query functions) ---- */
Expand Down
1 change: 1 addition & 0 deletions src/backend_win/hcs_vm.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ typedef struct {
wchar_t os_type[32]; /* L"Windows" or L"Linux" */
wchar_t image_path[MAX_PATH]; /* ISO path */
wchar_t vhdx_path[MAX_PATH]; /* will be created if doesn't exist */
wchar_t storage_folder[MAX_PATH]; /* empty = default; transient, not persisted */
DWORD ram_mb;
DWORD hdd_gb;
DWORD cpu_cores;
Expand Down
Loading