From 200652fc8fd22f0620fb02d1f2c213682d13fd39 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Thu, 27 Aug 2026 11:17:13 -0700 Subject: [PATCH] feat: support scalar union policy types - allow a policy key to declare a non-empty union of scalar native types - read exact boolean/string/number representations through one per-key native policy - preserve machine-over-user precedence and reject unforced macOS preferences - keep existing single-type declarations on their established readers - pin GitHub Windows CI to the Windows 2022 image supported by Node 22's node-gyp - prepare the 1.5.0 package release Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- binding.gyp | 6 ++- index.d.ts | 20 +++++--- package-lock.json | 4 +- package.json | 2 +- src/PolicyWatcher.hh | 1 + src/linux/PolicyWatcher.cc | 1 + src/macos/PolicyWatcher.cc | 6 +++ src/macos/PreferencesPolicy.hh | 9 ++++ src/macos/UnionPolicy.cc | 73 +++++++++++++++++++++++++++ src/macos/UnionPolicy.hh | 36 +++++++++++++ src/main.cc | 42 +++++++++++----- src/windows/PolicyWatcher.cc | 6 +++ src/windows/UnionPolicy.cc | 92 ++++++++++++++++++++++++++++++++++ src/windows/UnionPolicy.hh | 34 +++++++++++++ 15 files changed, 308 insertions(+), 26 deletions(-) create mode 100644 src/macos/UnionPolicy.cc create mode 100644 src/macos/UnionPolicy.hh create mode 100644 src/windows/UnionPolicy.cc create mode 100644 src/windows/UnionPolicy.hh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38cafaa..a4e3708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-2022] node-version: [22.x] steps: - name: Checkout diff --git a/binding.gyp b/binding.gyp index 46dbbd3..1d8677a 100644 --- a/binding.gyp +++ b/binding.gyp @@ -18,7 +18,8 @@ "src/macos/PolicyWatcher.cc", "src/macos/StringPolicy.cc", "src/macos/NumberPolicy.cc", - "src/macos/BooleanPolicy.cc" + "src/macos/BooleanPolicy.cc", + "src/macos/UnionPolicy.cc" ], "defines": [ "MACOS", @@ -48,7 +49,8 @@ "src/windows/PolicyWatcher.cc", "src/windows/StringPolicy.cc", "src/windows/NumberPolicy.cc", - "src/windows/BooleanPolicy.cc" + "src/windows/BooleanPolicy.cc", + "src/windows/UnionPolicy.cc" ], "defines": [ "WINDOWS" diff --git a/index.d.ts b/index.d.ts index 456d946..58afa7d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,9 +10,11 @@ interface Watcher { type StringPolicy = { type: "string" }; type NumberPolicy = { type: "number" }; type BooleanPolicy = { type: "boolean" }; +type PolicyType = "string" | "number" | "boolean"; +type UnionPolicy = { type: readonly [PolicyType, ...PolicyType[]] }; export interface Policies { - [policyName: string]: StringPolicy | NumberPolicy | BooleanPolicy; + [policyName: string]: StringPolicy | NumberPolicy | BooleanPolicy | UnionPolicy; } export interface WatcherOptions { @@ -23,15 +25,17 @@ export interface WatcherOptions { export type PolicyUpdate = { [K in keyof T]: | undefined - | (T[K] extends StringPolicy - ? string - : (T[K] extends BooleanPolicy - ? boolean - : T[K] extends NumberPolicy - ? number - : never)); + | (T[K]["type"] extends readonly PolicyType[] + ? PolicyTypeValue + : PolicyTypeValue); }; +type PolicyTypeValue = + T extends "string" ? string : + T extends "boolean" ? boolean : + T extends "number" ? number : + never; + export function createWatcher( productName: string, policies: T, diff --git a/package-lock.json b/package-lock.json index 55ea0ae..c7ca439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/policy-watcher", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/policy-watcher", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "bindings": "^1.5.0", diff --git a/package.json b/package.json index cd53cd7..db90d51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vscode/policy-watcher", - "version": "1.4.0", + "version": "1.5.0", "description": "", "main": "index.js", "repository": { diff --git a/src/PolicyWatcher.hh b/src/PolicyWatcher.hh index 35c34d6..b6ee733 100644 --- a/src/PolicyWatcher.hh +++ b/src/PolicyWatcher.hh @@ -35,6 +35,7 @@ public: void AddStringPolicy(const std::string name); void AddNumberPolicy(const std::string name); void AddBooleanPolicy(const std::string name); + void AddUnionPolicy(const std::string name, const std::vector &types); void OnExecute(Napi::Env env); void Execute(const ExecutionProgress &progress); diff --git a/src/linux/PolicyWatcher.cc b/src/linux/PolicyWatcher.cc index 1ea1dbc..7c9e6e4 100644 --- a/src/linux/PolicyWatcher.cc +++ b/src/linux/PolicyWatcher.cc @@ -20,6 +20,7 @@ PolicyWatcher::~PolicyWatcher() void PolicyWatcher::AddStringPolicy(const std::string name) {} void PolicyWatcher::AddNumberPolicy(const std::string name) {} void PolicyWatcher::AddBooleanPolicy(const std::string name) {} +void PolicyWatcher::AddUnionPolicy(const std::string name, const std::vector &types) {} void PolicyWatcher::OnExecute(Napi::Env env) {} void PolicyWatcher::Execute(const ExecutionProgress &progress) {} void PolicyWatcher::OnProgress(const Policy *const *policies, size_t count) {} diff --git a/src/macos/PolicyWatcher.cc b/src/macos/PolicyWatcher.cc index 468e78d..8ea468e 100644 --- a/src/macos/PolicyWatcher.cc +++ b/src/macos/PolicyWatcher.cc @@ -7,6 +7,7 @@ #include "StringPolicy.hh" #include "NumberPolicy.hh" #include "BooleanPolicy.hh" +#include "UnionPolicy.hh" #include using namespace Napi; @@ -57,6 +58,11 @@ void PolicyWatcher::AddBooleanPolicy(const std::string name) policies.push_back(std::make_unique(name, productName)); } +void PolicyWatcher::AddUnionPolicy(const std::string name, const std::vector &types) +{ + policies.push_back(std::make_unique(name, productName, types)); +} + void PolicyWatcher::OnExecute(Napi::Env env) { AsyncProgressQueueWorker::OnExecute(env); diff --git a/src/macos/PreferencesPolicy.hh b/src/macos/PreferencesPolicy.hh index a9a578e..9412752 100644 --- a/src/macos/PreferencesPolicy.hh +++ b/src/macos/PreferencesPolicy.hh @@ -31,6 +31,15 @@ public: PolicyRefreshResult refresh() { + if (!CFPreferencesAppValueIsForced(key, appID)) + { + if (!value.has_value()) + return PolicyRefreshResult::NotSet; + + value.reset(); + return PolicyRefreshResult::Removed; + } + auto newValue = read(); // Check for no value or removal diff --git a/src/macos/UnionPolicy.cc b/src/macos/UnionPolicy.cc new file mode 100644 index 0000000..e2a4ee1 --- /dev/null +++ b/src/macos/UnionPolicy.cc @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +#include "UnionPolicy.hh" +#include + +UnionPolicy::UnionPolicy(const std::string name, const std::string &productName, const std::vector &types) + : Policy(name), + appID(CFStringCreateWithCString(nullptr, productName.c_str(), kCFStringEncodingUTF8)), + key(CFStringCreateWithCString(nullptr, name.c_str(), kCFStringEncodingUTF8)) +{ + for (const auto &type : types) { + acceptsBoolean = acceptsBoolean || type == "boolean"; + acceptsNumber = acceptsNumber || type == "number"; + acceptsString = acceptsString || type == "string"; + } +} + +UnionPolicy::~UnionPolicy() +{ + CFRelease(appID); + CFRelease(key); +} + +PolicyRefreshResult UnionPolicy::refresh() +{ + auto next = read(); + if (value == next) + return value.has_value() ? PolicyRefreshResult::Unchanged : PolicyRefreshResult::NotSet; + auto removed = value.has_value() && !next.has_value(); + value = next; + return removed ? PolicyRefreshResult::Removed : PolicyRefreshResult::Updated; +} + +Napi::Value UnionPolicy::getValue(Napi::Env env) const +{ + if (!value.has_value()) + return env.Undefined(); + if (std::holds_alternative(*value)) + return Napi::Boolean::New(env, std::get(*value)); + if (std::holds_alternative(*value)) + return Napi::Number::New(env, std::get(*value)); + return Napi::String::New(env, std::get(*value)); +} + +std::optional UnionPolicy::read() const +{ + if (!CFPreferencesAppValueIsForced(key, appID)) + return std::nullopt; + auto pref = CFPreferencesCopyAppValue(key, appID); + if (pref == nullptr) + return std::nullopt; + + std::optional result; + auto type = CFGetTypeID(pref); + if (type == CFBooleanGetTypeID() && acceptsBoolean) { + result = pref == kCFBooleanTrue; + } else if (type == CFNumberGetTypeID() && acceptsNumber) { + long long number; + if (CFNumberGetValue(static_cast(pref), kCFNumberLongLongType, &number)) + result = static_cast(number); + } else if (type == CFStringGetTypeID() && acceptsString) { + CFIndex length = CFStringGetLength(static_cast(pref)); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + std::vector buffer(maxSize); + if (CFStringGetCString(static_cast(pref), buffer.data(), maxSize, kCFStringEncodingUTF8)) + result = std::string(buffer.data()); + } + CFRelease(pref); + return result; +} diff --git a/src/macos/UnionPolicy.hh b/src/macos/UnionPolicy.hh new file mode 100644 index 0000000..4004d6e --- /dev/null +++ b/src/macos/UnionPolicy.hh @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +#ifndef UNION_POLICY_H +#define UNION_POLICY_H + +#include +#include +#include +#include +#include +#include "../Policy.hh" + +using UnionPolicyValue = std::variant; + +class UnionPolicy : public Policy +{ +public: + UnionPolicy(const std::string name, const std::string &productName, const std::vector &types); + ~UnionPolicy(); + PolicyRefreshResult refresh(); + Napi::Value getValue(Napi::Env env) const; + +private: + std::optional read() const; + CFStringRef appID; + CFStringRef key; + bool acceptsBoolean = false; + bool acceptsNumber = false; + bool acceptsString = false; + std::optional value; +}; + +#endif diff --git a/src/main.cc b/src/main.cc index 9da17bd..2f51c16 100644 --- a/src/main.cc +++ b/src/main.cc @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ #include +#include #include #include "Policy.hh" @@ -67,21 +68,38 @@ Value CreateWatcher(const CallbackInfo &info) auto rawPolicy = rawPolicyValue.As(); auto rawPolicyType = rawPolicy.Get("type"); + std::vector policyTypes; + if (rawPolicyType.IsString()) { + policyTypes.push_back(std::string(rawPolicyType.As())); + } else if (rawPolicyType.IsArray()) { + auto rawPolicyTypes = rawPolicyType.As(); + if (rawPolicyTypes.Length() == 0) + throw TypeError::New(env, "Expected policy type array to be non-empty"); + for (uint32_t i = 0; i < rawPolicyTypes.Length(); i++) { + auto rawType = rawPolicyTypes.Get(i); + if (!rawType.IsString()) + throw TypeError::New(env, "Expected policy type array entries to be strings"); + auto type = std::string(rawType.As()); + if (std::find(policyTypes.begin(), policyTypes.end(), type) == policyTypes.end()) + policyTypes.push_back(type); + } + } else { + throw TypeError::New(env, "Expected policy type to be a string or non-empty string array"); + } - if (!rawPolicyType.IsString()) - throw TypeError::New(env, "Expected policy type to be string"); - - auto policyType = std::string(rawPolicyType.As()); - - if (policyType == "string") { - watcher->AddStringPolicy(rawPolicyName.As()); + for (const auto &policyType : policyTypes) { + if (policyType != "string" && policyType != "number" && policyType != "boolean") + throw TypeError::New(env, "Unknown policy type '" + policyType + "'"); } - else if (policyType == "number") { - watcher->AddNumberPolicy(rawPolicyName.As()); - } else if (policyType == "boolean") { - watcher->AddBooleanPolicy(rawPolicyName.As()); + + if (policyTypes.size() > 1) { + watcher->AddUnionPolicy(rawPolicyName.As(), policyTypes); + } else if (policyTypes[0] == "string") { + watcher->AddStringPolicy(rawPolicyName.As()); + } else if (policyTypes[0] == "number") { + watcher->AddNumberPolicy(rawPolicyName.As()); } else { - throw TypeError::New(env, "Unknown policy type '" + policyType + "'"); + watcher->AddBooleanPolicy(rawPolicyName.As()); } } diff --git a/src/windows/PolicyWatcher.cc b/src/windows/PolicyWatcher.cc index 85cfeb6..0157269 100644 --- a/src/windows/PolicyWatcher.cc +++ b/src/windows/PolicyWatcher.cc @@ -9,6 +9,7 @@ #include "StringPolicy.hh" #include "NumberPolicy.hh" #include "BooleanPolicy.hh" +#include "UnionPolicy.hh" using namespace Napi; @@ -43,6 +44,11 @@ void PolicyWatcher::AddBooleanPolicy(const std::string name) policies.push_back(std::make_unique(name, productName, registryPath)); } +void PolicyWatcher::AddUnionPolicy(const std::string name, const std::vector &types) +{ + policies.push_back(std::make_unique(name, productName, registryPath, types)); +} + void PolicyWatcher::OnExecute(Napi::Env env) { if ((handles[0] = CreateEvent(NULL, false, false, NULL)) == NULL) diff --git a/src/windows/UnionPolicy.cc b/src/windows/UnionPolicy.cc new file mode 100644 index 0000000..cd65cc3 --- /dev/null +++ b/src/windows/UnionPolicy.cc @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +#include "UnionPolicy.hh" +#include +#include + +UnionPolicy::UnionPolicy(const std::string name, const std::string &productName, const std::string &customRegistryPath, const std::vector &types) + : Policy(name), + registryKey(customRegistryPath.empty() ? ("Software\\Policies\\Microsoft\\" + productName) : customRegistryPath) +{ + for (const auto &type : types) { + acceptsBoolean = acceptsBoolean || type == "boolean"; + acceptsNumber = acceptsNumber || type == "number"; + acceptsString = acceptsString || type == "string"; + } +} + +PolicyRefreshResult UnionPolicy::refresh() +{ + auto next = read(HKEY_LOCAL_MACHINE); + if (!next.has_value()) + next = read(HKEY_CURRENT_USER); + + if (value == next) + return value.has_value() ? PolicyRefreshResult::Unchanged : PolicyRefreshResult::NotSet; + auto removed = value.has_value() && !next.has_value(); + value = next; + return removed ? PolicyRefreshResult::Removed : PolicyRefreshResult::Updated; +} + +Napi::Value UnionPolicy::getValue(Napi::Env env) const +{ + if (!value.has_value()) + return env.Undefined(); + if (std::holds_alternative(*value)) + return Napi::Boolean::New(env, std::get(*value)); + if (std::holds_alternative(*value)) + return Napi::Number::New(env, std::get(*value)); + return Napi::String::New(env, std::get(*value)); +} + +std::optional UnionPolicy::read(HKEY root) const +{ + HKEY key; + if (RegOpenKeyEx(root, registryKey.c_str(), 0, KEY_READ, &key) != ERROR_SUCCESS) + return std::nullopt; + + DWORD type; + DWORD size = 0; + auto status = RegQueryValueEx(key, name.c_str(), nullptr, &type, nullptr, &size); + if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA) { + RegCloseKey(key); + return std::nullopt; + } + + std::vector buffer(size); + status = RegQueryValueEx(key, name.c_str(), nullptr, &type, buffer.data(), &size); + RegCloseKey(key); + if (status != ERROR_SUCCESS) + return std::nullopt; + + if (type == REG_DWORD && acceptsBoolean && size == sizeof(DWORD)) + return *reinterpret_cast(buffer.data()) != 0; + if (type == REG_QWORD && acceptsNumber && size == sizeof(long long)) + return static_cast(*reinterpret_cast(buffer.data())); + if ((type == REG_SZ || type == REG_MULTI_SZ) && acceptsString) { + if (type == REG_SZ && size == 0) + return std::string(); + const char *begin = reinterpret_cast(buffer.data()); + const char *end = begin + size; + if (type == REG_SZ) { + const char *terminator = std::find(begin, end, '\0'); + return std::string(begin, terminator); + } + if (size < 2 || end[-1] != '\0' || end[-2] != '\0') + return std::nullopt; + std::string result; + const char *current = begin; + while (current < end && *current != '\0') { + const char *terminator = std::find(current, end, '\0'); + if (!result.empty()) + result += '\n'; + result.append(current, terminator); + current = terminator == end ? end : terminator + 1; + } + return result; + } + return std::nullopt; +} diff --git a/src/windows/UnionPolicy.hh b/src/windows/UnionPolicy.hh new file mode 100644 index 0000000..494f394 --- /dev/null +++ b/src/windows/UnionPolicy.hh @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +#ifndef UNION_POLICY_H +#define UNION_POLICY_H + +#include +#include +#include +#include +#include +#include "../Policy.hh" + +using UnionPolicyValue = std::variant; + +class UnionPolicy : public Policy +{ +public: + UnionPolicy(const std::string name, const std::string &productName, const std::string ®istryPath, const std::vector &types); + PolicyRefreshResult refresh(); + Napi::Value getValue(Napi::Env env) const; + +private: + std::optional read(HKEY root) const; + const std::string registryKey; + bool acceptsBoolean = false; + bool acceptsNumber = false; + bool acceptsString = false; + std::optional value; +}; + +#endif