/* Windows side of os_compat: every string crossing the OS boundary is UTF-8 * inside cst-agent and UTF-16 at the Win32 W APIs. The embedded manifest also * sets the process code page to UTF-8, so CRT calls (fopen, getenv, argv) * see UTF-8 too. */ #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include "os_compat.h" #include "cst_util.h" #include "os_internal.h" #include #include #include #include #include #include #include wchar_t *os_utf8_to_wide(const char *s) { if (!s) return NULL; int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0); if (n <= 0) return NULL; wchar_t *w = malloc((size_t)n * sizeof(wchar_t)); if (w && MultiByteToWideChar(CP_UTF8, 0, s, -1, w, n) <= 0) { free(w); return NULL; } return w; } char *os_wide_to_utf8(const wchar_t *w) { if (!w) return NULL; int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL); if (n <= 0) return NULL; char *s = malloc((size_t)n); if (s && WideCharToMultiByte(CP_UTF8, 0, w, -1, s, n, NULL, NULL) <= 0) { free(s); return NULL; } return s; } static UINT g_old_cp, g_old_out_cp; static void console_restore(void) { if (g_old_cp) SetConsoleCP(g_old_cp); if (g_old_out_cp) SetConsoleOutputCP(g_old_out_cp); } int os_console_init(void) { static int done; if (done) return GetACP() == CP_UTF8 ? 0 : -1; done = 1; g_old_cp = GetConsoleCP(); g_old_out_cp = GetConsoleOutputCP(); if (g_old_cp || g_old_out_cp) { SetConsoleCP(CP_UTF8); SetConsoleOutputCP(CP_UTF8); atexit(console_restore); } setlocale(LC_ALL, ".UTF-8"); /* children inherit these: Python otherwise writes the ANSI code page to pipes */ if (!getenv("PYTHONUTF8")) _putenv_s("PYTHONUTF8", "1"); if (!getenv("PYTHONIOENCODING")) _putenv_s("PYTHONIOENCODING", "utf-8"); return GetACP() == CP_UTF8 ? 0 : -1; } /* The system locale's code page. GetOEMCP()/GetACP() say 65001 once the * manifest makes this process UTF-8, but children without one (cmd, most * console tools) still write the system's OEM code page, e.g. 936. */ static UINT system_cp(LCTYPE which) { wchar_t b[8]; if (!GetLocaleInfoW(LOCALE_SYSTEM_DEFAULT, which, b, 8)) return 0; return (UINT)wcstoul(b, NULL, 10); } char *os_output_to_utf8(const char *s, size_t n, size_t *out_len) { if (!s || cst_utf8_valid(s, n)) return NULL; UINT cps[] = {GetConsoleOutputCP(), system_cp(LOCALE_IDEFAULTCODEPAGE), system_cp(LOCALE_IDEFAULTANSICODEPAGE), GetOEMCP(), GetACP()}; for (size_t i = 0; i < sizeof(cps) / sizeof(cps[0]); i++) { UINT cp = cps[i]; if (!cp || cp == CP_UTF8) continue; int wn = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0); if (wn <= 0) continue; wchar_t *w = malloc((size_t)wn * sizeof(wchar_t)); if (!w) return NULL; MultiByteToWideChar(cp, 0, s, (int)n, w, wn); int un = WideCharToMultiByte(CP_UTF8, 0, w, wn, NULL, 0, NULL, NULL); char *u = un > 0 ? malloc((size_t)un + 1) : NULL; if (u) { WideCharToMultiByte(CP_UTF8, 0, w, wn, u, un, NULL, NULL); u[un] = 0; if (out_len) *out_len = (size_t)un; } free(w); return u; } return NULL; } /* --- paths --------------------------------------------------------------- */ /* UTF-8 path -> wide, with the \\?\ prefix once it gets near MAX_PATH so * deep trees work without the LongPathsEnabled policy. */ static wchar_t *wpath(const char *p) { wchar_t *w = os_utf8_to_wide(p); if (!w || wcslen(w) < MAX_PATH - 12) return w; DWORD n = GetFullPathNameW(w, 0, NULL, NULL); wchar_t *f = n ? malloc(n * sizeof(wchar_t)) : NULL; wchar_t *out = n ? malloc((n + 8) * sizeof(wchar_t)) : NULL; if (!f || !out || GetFullPathNameW(w, n, f, NULL) == 0) { free(f); free(out); return w; } free(w); if (wcsncmp(f, L"\\\\?\\", 4) == 0) { wcscpy(out, f); } else if (f[0] == L'\\' && f[1] == L'\\') { /* \\srv\share -> \\?\UNC\srv\share */ wcscpy(out, L"\\\\?\\UNC"); wcscat(out, f + 1); } else { wcscpy(out, L"\\\\?\\"); wcscat(out, f); } free(f); return out; } /* Wide path from the OS -> UTF-8 with '/' separators, no \\?\ prefix and a * lowercase drive letter (stable for prefix compares). */ static int path_out(const wchar_t *w, char *out, size_t out_sz) { const char *lead = ""; if (wcsncmp(w, L"\\\\?\\UNC\\", 8) == 0) { lead = "\\\\"; /* \\?\UNC\srv\share -> \\srv\share */ w += 8; } else if (wcsncmp(w, L"\\\\?\\", 4) == 0) { w += 4; } char *u = os_wide_to_utf8(w); if (!u) return -1; int n = snprintf(out, out_sz, "%s%s", lead, u); free(u); if (n < 0 || (size_t)n >= out_sz) return -1; os_path_norm_slashes(out); if (out[0] >= 'A' && out[0] <= 'Z' && out[1] == ':') out[0] = (char)(out[0] - 'A' + 'a'); return 0; } int os_realpath(const char *path, char *out, size_t out_sz) { if (!path || !path[0] || !out || out_sz < 2) return -1; wchar_t *w = wpath(path); if (!w) return -1; HANDLE h = CreateFileW(w, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); free(w); if (h == INVALID_HANDLE_VALUE) return -1; wchar_t stackbuf[MAX_PATH + 16], *buf = stackbuf; DWORD n = GetFinalPathNameByHandleW(h, buf, MAX_PATH + 16, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); if (n >= MAX_PATH + 16) { buf = malloc((n + 1) * sizeof(wchar_t)); n = buf ? GetFinalPathNameByHandleW(h, buf, n + 1, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS) : 0; } CloseHandle(h); int rc = n ? path_out(buf, out, out_sz) : -1; if (buf != stackbuf) free(buf); return rc; } /* NTFS names are case-insensitive: compare the prefix the way the OS does. */ int os_path_under(const char *root, const char *path) { if (!root || !path) return 0; wchar_t *wr = os_utf8_to_wide(root), *wp = os_utf8_to_wide(path); int ok = 0; if (wr && wp) { size_t rl = wcslen(wr); if (wcslen(wp) >= rl && (rl == 0 || CompareStringOrdinal(wr, (int)rl, wp, (int)rl, TRUE) == CSTR_EQUAL)) { wchar_t c = wp[rl]; ok = c == 0 || c == L'/' || c == L'\\' || (rl && (wr[rl - 1] == L'/' || wr[rl - 1] == L'\\')); } } free(wr); free(wp); return ok; } static int reparse_is_link(DWORD attrs, DWORD tag) { return (attrs & FILE_ATTRIBUTE_REPARSE_POINT) && (tag == IO_REPARSE_TAG_SYMLINK || tag == IO_REPARSE_TAG_MOUNT_POINT); } /* --- directories ------------------------------------------------------- */ struct os_dir { HANDLE h; int pending; /* fd holds an entry not yet returned */ WIN32_FIND_DATAW fd; char name[MAX_PATH * 4]; os_dirent_t ent; }; os_dir_t *os_dir_open(const char *path) { if (!path || !path[0]) return NULL; size_t L = strlen(path); char *pat = malloc(L + 3); if (!pat) return NULL; memcpy(pat, path, L); if (path[L - 1] != '/' && path[L - 1] != '\\') pat[L++] = '\\'; pat[L++] = '*'; pat[L] = 0; wchar_t *w = wpath(pat); free(pat); os_dir_t *d = w ? calloc(1, sizeof(*d)) : NULL; if (!d) { free(w); return NULL; } d->h = FindFirstFileExW(w, FindExInfoBasic, &d->fd, FindExSearchNameMatch, NULL, FIND_FIRST_EX_LARGE_FETCH); free(w); if (d->h == INVALID_HANDLE_VALUE) { DWORD e = GetLastError(); if (e != ERROR_FILE_NOT_FOUND) { /* an empty drive root has no entries */ free(d); return NULL; } } else { d->pending = 1; } return d; } const os_dirent_t *os_dir_next(os_dir_t *d) { if (!d || d->h == INVALID_HANDLE_VALUE) return NULL; for (;;) { if (!d->pending && !FindNextFileW(d->h, &d->fd)) return NULL; d->pending = 0; const wchar_t *n = d->fd.cFileName; if (wcscmp(n, L".") == 0 || wcscmp(n, L"..") == 0) continue; if (WideCharToMultiByte(CP_UTF8, 0, n, -1, d->name, (int)sizeof(d->name), NULL, NULL) <= 0) continue; DWORD a = d->fd.dwFileAttributes; d->ent.name = d->name; d->ent.is_link = reparse_is_link(a, d->fd.dwReserved0); d->ent.is_dir = !d->ent.is_link && (a & FILE_ATTRIBUTE_DIRECTORY); return &d->ent; } } void os_dir_close(os_dir_t *d) { if (!d) return; if (d->h != INVALID_HANDLE_VALUE) FindClose(d->h); free(d); } static double filetime_secs(FILETIME ft) { ULONGLONG t = ((ULONGLONG)ft.dwHighDateTime << 32) | ft.dwLowDateTime; return (double)t / 1e7 - 11644473600.0; } int os_lstat(const char *path, os_stat_t *st) { memset(st, 0, sizeof(*st)); wchar_t *w = path ? wpath(path) : NULL; if (!w) return -1; WIN32_FILE_ATTRIBUTE_DATA fa; if (!GetFileAttributesExW(w, GetFileExInfoStandard, &fa)) { free(w); return -1; } DWORD a = fa.dwFileAttributes; if (a & FILE_ATTRIBUTE_REPARSE_POINT) { WIN32_FIND_DATAW fd; HANDLE h = FindFirstFileW(w, &fd); if (h != INVALID_HANDLE_VALUE) { st->is_link = reparse_is_link(a, fd.dwReserved0); FindClose(h); } } free(w); st->is_dir = !st->is_link && (a & FILE_ATTRIBUTE_DIRECTORY); st->is_reg = !st->is_link && !(a & FILE_ATTRIBUTE_DIRECTORY); st->size = (long long)(((ULONGLONG)fa.nFileSizeHigh << 32) | fa.nFileSizeLow); st->mtime = filetime_secs(fa.ftLastWriteTime); return 0; } int os_rename_replace(const char *from, const char *to) { wchar_t *wf = wpath(from), *wt = wpath(to); int ok = wf && wt && MoveFileExW(wf, wt, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED); free(wf); free(wt); return ok ? 0 : -1; } /* --- processes ----------------------------------------------------------- */ /* One argument the way CommandLineToArgvW and the CRT split it again. */ static void cmdline_arg(cst_buf_t *b, const char *a) { if (a[0] && !strpbrk(a, " \t\n\v\"")) { cst_buf_puts(b, a); return; } cst_buf_puts(b, "\""); for (const char *p = a;; p++) { size_t bs = 0; while (*p == '\\') { bs++; p++; } if (!*p) { for (size_t i = 0; i < bs * 2; i++) cst_buf_puts(b, "\\"); break; } for (size_t i = 0; i < (*p == '"' ? bs * 2 + 1 : bs); i++) cst_buf_puts(b, "\\"); cst_buf_append(b, p, 1); } cst_buf_puts(b, "\""); } static wchar_t *build_cmdline(const char *const argv[]) { cst_buf_t b = {0}; for (int i = 0; argv[i]; i++) { if (i) cst_buf_puts(&b, " "); cmdline_arg(&b, argv[i]); } wchar_t *w = b.buf ? os_utf8_to_wide(b.buf) : NULL; cst_buf_free(&b); return w; } static size_t env_name_len(const wchar_t *e) { const wchar_t *eq = wcschr(e + 1, L'='); /* "=C:=C:\x" hides a leading '=' */ return eq ? (size_t)(eq - e) : wcslen(e); } static int env_cmp(const void *a, const void *b) { const wchar_t *x = *(const wchar_t *const *)a, *y = *(const wchar_t *const *)b; return CompareStringOrdinal(x, (int)env_name_len(x), y, (int)env_name_len(y), TRUE) - CSTR_EQUAL; } /* The inherited environment with env_add ("KEY=VALUE") layered on top, as a * sorted CREATE_UNICODE_ENVIRONMENT block; NULL = inherit unchanged. */ static wchar_t *env_block(const char *const env_add[]) { if (!env_add || !env_add[0]) return NULL; size_t nadd = 0, ncur = 0; while (env_add[nadd]) nadd++; wchar_t *cur = GetEnvironmentStringsW(); for (const wchar_t *e = cur; e && *e; e += wcslen(e) + 1) ncur++; wchar_t **add = calloc(nadd, sizeof(*add)); wchar_t **v = calloc(ncur + nadd, sizeof(*v)); wchar_t *blk = NULL; size_t n = 0, total = 1; if (!add || !v) goto out; for (size_t i = 0; i < nadd; i++) if (!(add[i] = os_utf8_to_wide(env_add[i]))) goto out; for (wchar_t *e = cur; e && *e; e += wcslen(e) + 1) { int shadowed = 0; for (size_t i = 0; i < nadd && !shadowed; i++) shadowed = env_cmp(&e, &add[i]) == 0; if (!shadowed) v[n++] = e; } for (size_t i = 0; i < nadd; i++) { int dup = 0; /* a later entry for the same name wins */ for (size_t k = i + 1; k < nadd && !dup; k++) dup = env_cmp(&add[i], &add[k]) == 0; if (!dup) v[n++] = add[i]; } qsort(v, n, sizeof(*v), env_cmp); for (size_t i = 0; i < n; i++) total += wcslen(v[i]) + 1; blk = malloc(total * sizeof(wchar_t)); if (blk) { wchar_t *o = blk; for (size_t i = 0; i < n; i++) { size_t l = wcslen(v[i]) + 1; memcpy(o, v[i], l * sizeof(wchar_t)); o += l; } *o = 0; } out: for (size_t i = 0; add && i < nadd; i++) free(add[i]); free(add); free(v); if (cur) FreeEnvironmentStringsW(cur); return blk; } static HANDLE new_job(void) { HANDLE job = CreateJobObjectW(NULL, NULL); if (!job) return NULL; JOBOBJECT_EXTENDED_LIMIT_INFORMATION li; memset(&li, 0, sizeof(li)); li.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION; SetInformationJobObject(job, JobObjectExtendedLimitInformation, &li, sizeof(li)); return job; } static DWORD job_active(HANDLE job) { JOBOBJECT_BASIC_ACCOUNTING_INFORMATION ai; if (!job || !QueryInformationJobObject(job, JobObjectBasicAccountingInformation, &ai, sizeof(ai), NULL)) return 0; return ai.ActiveProcesses; } /* Children still running after the shell exited. A child started without * our console (the sandbox) has its own conhost.exe, which leaves the job a * moment after its client, so give the job a short while to drain. */ static int job_lingers(HANDLE job) { for (int i = 0; i < 50 && job_active(job) > 0; i++) Sleep(10); return job_active(job) > 0; } /* Start argv suspended, put it in a fresh kill-on-close job, then resume. * Only in/out/err are inherited (PROC_THREAD_ATTRIBUTE_HANDLE_LIST), so a * concurrent spawn on another thread never leaks into this child. */ static int spawn(const char *cwd, const char *const argv[], const char *const env_add[], HANDLE token, HANDLE in, HANDLE out, HANDLE err, HANDLE *proc, HANDLE *job, DWORD *error) { *proc = *job = NULL; *error = ERROR_OUTOFMEMORY; wchar_t *cmd = build_cmdline(argv); wchar_t *wcwd = cwd && cwd[0] ? os_utf8_to_wide(cwd) : NULL; wchar_t *env = env_block(env_add); HANDLE inherit[3]; int ninh = 0; HANDLE std3[3] = {in, out, err}; for (int i = 0; i < 3; i++) { int seen = !std3[i] || std3[i] == INVALID_HANDLE_VALUE; for (int k = 0; k < ninh && !seen; k++) seen = inherit[k] == std3[i]; if (!seen) inherit[ninh++] = std3[i]; } SIZE_T asz = 0; InitializeProcThreadAttributeList(NULL, 1, 0, &asz); LPPROC_THREAD_ATTRIBUTE_LIST attrs = malloc(asz); int ok = 0; if (cmd && (!cwd || !cwd[0] || wcwd) && (!env_add || !env_add[0] || env) && attrs && InitializeProcThreadAttributeList(attrs, 1, 0, &asz)) { if (UpdateProcThreadAttribute(attrs, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherit, (SIZE_T)ninh * sizeof(HANDLE), NULL, NULL)) { STARTUPINFOEXW si; memset(&si, 0, sizeof(si)); si.StartupInfo.cb = sizeof(si); si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; si.StartupInfo.hStdInput = in; si.StartupInfo.hStdOutput = out; si.StartupInfo.hStdError = err; si.lpAttributeList = attrs; /* Share our console when there is one (children then see its UTF-8 * code page); the sandbox token cannot attach to it. */ DWORD flags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP | EXTENDED_STARTUPINFO_PRESENT; if (token || !GetConsoleWindow()) flags |= CREATE_NO_WINDOW; PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); ok = token ? CreateProcessAsUserW(token, NULL, cmd, NULL, NULL, TRUE, flags, env, wcwd, &si.StartupInfo, &pi) : CreateProcessW(NULL, cmd, NULL, NULL, TRUE, flags, env, wcwd, &si.StartupInfo, &pi); if (ok) { *job = new_job(); if (*job && !AssignProcessToJobObject(*job, pi.hProcess)) { CloseHandle(*job); *job = NULL; } ResumeThread(pi.hThread); CloseHandle(pi.hThread); *proc = pi.hProcess; } else { *error = GetLastError(); } } DeleteProcThreadAttributeList(attrs); } free(attrs); free(cmd); free(wcwd); free(env); return ok ? 0 : -1; } int os_run_capture_token(const char *cwd, const char *const argv[], size_t max_out, os_run_result_t *r, void *token, const char *const env_add[]) { if (!argv || !argv[0] || !r) return -1; if (max_out == 0) max_out = 512 * 1024; memset(r, 0, sizeof(*r)); SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; HANDLE rd = NULL, wr = NULL; if (!CreatePipe(&rd, &wr, &sa, 64 * 1024)) return -1; SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0); HANDLE nul = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL); HANDLE proc, job; DWORD err; int rc = spawn(cwd, argv, env_add, (HANDLE)token, nul, wr, wr, &proc, &job, &err); CloseHandle(wr); if (nul != INVALID_HANDLE_VALUE) CloseHandle(nul); if (rc != 0) { CloseHandle(rd); if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) return -1; cst_buf_t b = {0}; cst_buf_printf(&b, "%s: command not found\n", argv[0]); r->out_len = b.len; r->out = cst_buf_steal(&b); r->exit_code = 127; return 0; } size_t cap = 4096, len = 0; char *buf = malloc(cap); if (!buf) { TerminateJobObject(job, 1); TerminateProcess(proc, 1); CloseHandle(rd); CloseHandle(proc); if (job) CloseHandle(job); return -1; } buf[0] = 0; int truncated = 0, killed = 0, reaped = 0, bg_killed = 0; double reaped_at = 0, killed_at = 0; /* no timeout unless one was asked for: a deadline far in the future keeps * every `now > deadline` check working without a second code path */ int timeout_s = os_run_timeout_effective(); double deadline = timeout_s > 0 ? os_now_ms() + timeout_s * 1000.0 : 1e18; int idle = 0; /* empty polls in a row: wait longer once output stalls */ for (;;) { DWORD avail = 0; if (!PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL)) break; /* every writer closed */ if (avail) { char tmp[65536]; DWORD n = 0; if (!ReadFile(rd, tmp, avail < sizeof(tmp) ? avail : sizeof(tmp), &n, NULL) || n == 0) break; os_cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated); os_observe_output(tmp, n); idle = 0; } else if (reaped) { Sleep(idle++ < 50 ? 1 : 20); } DWORD wait = avail ? 0 : idle++ < 50 ? 1 : 20; if (!reaped && WaitForSingleObject(proc, wait) == WAIT_OBJECT_0) { reaped = 1; reaped_at = os_now_ms(); } double now = os_now_ms(); /* The shell exited but a background child still holds the pipe * (`server &`): give it a moment, then end the tree. */ if (reaped && !killed && !bg_killed && now - reaped_at > 1000) { TerminateJobObject(job, 1); bg_killed = 1; killed_at = now; } if (!reaped && !killed && (os_cancel_requested() || now > deadline)) { killed = os_cancel_requested() ? 2 : 1; killed_at = now; TerminateJobObject(job, killed == 1 ? 124 : 130); if (!job) TerminateProcess(proc, killed == 1 ? 124 : 130); } /* a writer outside the job can keep the pipe open */ if ((killed || bg_killed) && now - killed_at > 3000) break; } CloseHandle(rd); WaitForSingleObject(proc, killed ? 5000 : INFINITE); DWORD code = 1; GetExitCodeProcess(proc, &code); if (!bg_killed && !killed && job_lingers(job)) bg_killed = 1; /* closing the job below ends them */ CloseHandle(proc); if (job) CloseHandle(job); size_t ulen = 0; char *u = os_output_to_utf8(buf, len, &ulen); if (u) { free(buf); buf = u; len = ulen; cap = ulen + 1; } if (truncated) os_cap_note(&buf, &len, &cap, "\n...(output truncated)\n"); if (bg_killed && !killed) os_cap_note(&buf, &len, &cap, "\n[cst: background processes left running were stopped]\n"); if (killed == 1) { char note[96]; snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n", os_run_timeout_effective()); os_cap_note(&buf, &len, &cap, note); code = 124; } else if (killed == 2) { os_cap_note(&buf, &len, &cap, "\n[cst: cancelled]\n"); code = 130; } r->out = buf; r->out_len = len; r->exit_code = (int)code; return 0; } int os_run_capture(const char *cwd, const char *const argv[], size_t max_out, os_run_result_t *r) { return os_run_capture_token(cwd, argv, max_out, r, NULL, NULL); } int os_run_capture_ex(const char *cwd, const char *const argv[], size_t max_out, os_run_result_t *r, os_child_hook_fn child_hook, void *ud) { (void)child_hook; (void)ud; return os_run_capture_token(cwd, argv, max_out, r, NULL, NULL); } int os_proc_spawn(const char *cwd, const char *const argv[], const char *const env_add[], const char *stderr_path, os_proc_t *p) { memset(p, 0, sizeof(*p)); if (!argv || !argv[0]) return -1; SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; HANDLE in_r = NULL, in_w = NULL, out_r = NULL, out_w = NULL; if (!CreatePipe(&in_r, &in_w, &sa, 64 * 1024)) return -1; if (!CreatePipe(&out_r, &out_w, &sa, 64 * 1024)) { CloseHandle(in_r); CloseHandle(in_w); return -1; } SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0); SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0); wchar_t *werr = stderr_path ? wpath(stderr_path) : NULL; HANDLE err = CreateFileW(werr ? werr : L"NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, werr ? CREATE_ALWAYS : OPEN_EXISTING, 0, NULL); free(werr); HANDLE proc, job; DWORD e; int rc = spawn(cwd, argv, env_add, NULL, in_r, out_w, err != INVALID_HANDLE_VALUE ? err : out_w, &proc, &job, &e); CloseHandle(in_r); CloseHandle(out_w); if (err != INVALID_HANDLE_VALUE) CloseHandle(err); if (rc != 0) { CloseHandle(in_w); CloseHandle(out_r); return -1; } p->proc = proc; p->job = job; p->in_w = in_w; p->out_r = out_r; return 0; } int os_proc_write(os_proc_t *p, const char *data, size_t n) { while (n > 0) { DWORD w = 0; if (!p->in_w || !WriteFile((HANDLE)p->in_w, data, (DWORD)n, &w, NULL) || w == 0) return -1; data += w; n -= w; } return 0; } long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms) { if (!p->out_r) return -1; double deadline = os_now_ms() + timeout_ms; DWORD avail = 0; for (;;) { if (!PeekNamedPipe((HANDLE)p->out_r, NULL, 0, NULL, &avail, NULL)) return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1; if (avail > 0) break; if (os_now_ms() >= deadline) return -2; Sleep(5); } DWORD r = 0; if (!ReadFile((HANDLE)p->out_r, buf, avail < n ? avail : (DWORD)n, &r, NULL)) return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1; return (long)r; } void os_proc_close_stdin(os_proc_t *p) { if (p->in_w) { CloseHandle((HANDLE)p->in_w); p->in_w = NULL; } } void os_proc_kill(os_proc_t *p) { os_proc_close_stdin(p); if (p->proc) { /* polite first: most servers exit when stdin closes */ if (WaitForSingleObject((HANDLE)p->proc, 500) != WAIT_OBJECT_0) { if (p->job) TerminateJobObject((HANDLE)p->job, 1); else TerminateProcess((HANDLE)p->proc, 1); } WaitForSingleObject((HANDLE)p->proc, 2000); CloseHandle((HANDLE)p->proc); p->proc = NULL; } if (p->job) { CloseHandle((HANDLE)p->job); /* ends anything the server left behind */ p->job = NULL; } if (p->out_r) { CloseHandle((HANDLE)p->out_r); p->out_r = NULL; } } void os_windows_version(char *out, size_t n) { typedef LONG(WINAPI * rtl_ver_fn)(OSVERSIONINFOW *); OSVERSIONINFOW vi; memset(&vi, 0, sizeof(vi)); vi.dwOSVersionInfoSize = sizeof(vi); HMODULE nt = GetModuleHandleW(L"ntdll.dll"); rtl_ver_fn fn = nt ? (rtl_ver_fn)(void (*)(void))GetProcAddress(nt, "RtlGetVersion") : NULL; SYSTEM_INFO si; GetNativeSystemInfo(&si); const char *arch = si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM64 ? "arm64" : "x86_64"; if (fn && fn(&vi) == 0) snprintf(out, n, "Windows %lu.%lu.%lu %s", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber, arch); else snprintf(out, n, "Windows %s", arch); } /* Mandatory label from an SDDL SACL such as "S:(ML;;NWNR;;;ME)". */ int os_win_set_label(const char *path, const char *sddl) { wchar_t *wp = wpath(path), *ws = os_utf8_to_wide(sddl); PSECURITY_DESCRIPTOR sd = NULL; int rc = -1; if (wp && ws && ConvertStringSecurityDescriptorToSecurityDescriptorW(ws, SDDL_REVISION_1, &sd, NULL)) { PACL sacl = NULL; BOOL present = FALSE, defaulted = FALSE; if (GetSecurityDescriptorSacl(sd, &present, &sacl, &defaulted) && present && SetNamedSecurityInfoW(wp, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION, NULL, NULL, NULL, sacl) == ERROR_SUCCESS) rc = 0; LocalFree(sd); } free(wp); free(ws); return rc; } int os_win_get_label(const char *path, char *out, size_t n) { wchar_t *wp = wpath(path); PSECURITY_DESCRIPTOR sd = NULL; wchar_t *ws = NULL; int rc = -1; if (wp && n > 2 && GetNamedSecurityInfoW(wp, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION, NULL, NULL, NULL, NULL, &sd) == ERROR_SUCCESS && ConvertSecurityDescriptorToStringSecurityDescriptorW( sd, SDDL_REVISION_1, LABEL_SECURITY_INFORMATION, &ws, NULL)) { char *s = os_wide_to_utf8(ws); if (s) { /* keep "S:" and the explicit ACEs; inherited ones carry the ID flag */ size_t o = 0; out[o++] = 'S'; out[o++] = ':'; rc = 0; for (const char *p = strchr(s, '('); p; p = strchr(p + 1, '(')) { const char *e = strchr(p, ')'); if (!e) break; /* (type;flags;rights;...): flags are two-letter codes */ const char *f = strchr(p, ';'); int inherited = 0; for (f = f && f < e ? f + 1 : e; f + 1 < e && *f != ';'; f += 2) inherited |= f[0] == 'I' && f[1] == 'D'; size_t len = (size_t)(e - p) + 1; if (inherited) continue; if (o + len >= n) { rc = -1; break; } memcpy(out + o, p, len); o += len; } out[o] = 0; free(s); } } if (ws) LocalFree(ws); if (sd) LocalFree(sd); free(wp); return rc; } int os_chmod_private(const char *path) { if (!path) return -1; return os_win_set_label(path, "S:(ML;;NWNRNX;;;ME)"); } /* --- clipboard ----------------------------------------------------------- */ int os_clipboard_set(const char *utf8, size_t n) { cst_buf_t b = {0}; /* CRLF is what Windows text consumers expect */ for (size_t i = 0; i < n; i++) { if (utf8[i] == '\n' && (i == 0 || utf8[i - 1] != '\r')) cst_buf_puts(&b, "\r"); cst_buf_append(&b, utf8 + i, 1); } if (!b.buf) cst_buf_puts(&b, ""); int wn = MultiByteToWideChar(CP_UTF8, 0, b.buf, (int)b.len, NULL, 0); HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, ((size_t)wn + 1) * sizeof(wchar_t)); wchar_t *w = h ? GlobalLock(h) : NULL; if (!w) { if (h) GlobalFree(h); cst_buf_free(&b); return -1; } MultiByteToWideChar(CP_UTF8, 0, b.buf, (int)b.len, w, wn); w[wn] = 0; GlobalUnlock(h); cst_buf_free(&b); if (!OpenClipboard(NULL)) { GlobalFree(h); return -1; } EmptyClipboard(); int ok = SetClipboardData(CF_UNICODETEXT, h) != NULL; if (!ok) GlobalFree(h); CloseClipboard(); return ok ? 0 : -1; } #endif /* _WIN32 */