Permissions-Policy Builder — C++ source
Build a Permissions-Policy header interactively. Control which browser features (camera, microphone, geolocation, etc.) your site can use.
This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Permissions-Policy header builder / parser.
//
// Language: C++17 (standard library only)
// Ported from src/lib/permissions-policy.ts (the canonical TypeScript
// implementation). display source — part of CosmoDev's polyglot tool pages.
//
// The Permissions-Policy header is a comma-separated list of directives:
// Permissions-Policy: geolocation=(self), camera=(), microphone=*, usb=(https://a.example)
// Each directive maps a browser feature to an allowlist. An empty allowlist
// `()` disables the feature outright; `*` allows it everywhere; `self` limits
// it to the page's own origin; anything else is a space-separated origin list.
//
// A policy is modeled as feature -> allowlist tokens:
// { geolocation: ["self"], camera: {}, usb: {"https://a.example"} }
// - empty => camera=() (disabled)
// - {"*"} => microphone=* (every origin)
// - {"self"} => geolocation=(self)
// Features absent from the map are absent from the header (browser default).
#include <algorithm>
#include <cctype>
#include <cmath>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace perms_policy {
enum class PrivacyImpact { High, Medium, Low };
struct FeatureInfo {
std::string name; ///< the directive token, e.g. "geolocation"
std::string description;
PrivacyImpact privacyImpact;
std::string defaultBrowserBehavior;
};
/// feature name -> allowlist tokens ({} = disabled, {"*"} = all origins).
using FeatureMap = std::map<std::string, std::vector<std::string>>;
const std::vector<FeatureInfo>& features() {
static const std::vector<FeatureInfo> FEATURES = {
// --- high privacy impact ---
{"camera", "Access the device camera for photos / video calls.", PrivacyImpact::High, "Same-origin only; prompts the user."},
{"microphone", "Capture audio from the device microphone.", PrivacyImpact::High, "Same-origin only; prompts the user."},
{"geolocation", "Read the precise GPS location of the visitor.", PrivacyImpact::High, "Same-origin only; prompts the user."},
{"display-capture", "Screen / window sharing via getDisplayMedia.", PrivacyImpact::High, "Same-origin only; prompts the user."},
{"idle-detection", "Detects when the user is away from the device — reveals usage patterns.", PrivacyImpact::High, "Disabled; prompts the user."},
{"serial", "Talk to serial devices (Arduinos, POS terminals) over a physical port.", PrivacyImpact::High, "Disabled; prompts the user."},
{"usb", "WebUSB — direct access to connected USB devices.", PrivacyImpact::High, "Disabled; prompts the user."},
{"hid", "Human Interface Devices — raw access to unusual keyboards, gamepads, sensors.", PrivacyImpact::High, "Disabled; prompts the user."},
{"xr-spatial-tracking", "Tracks head / hand position in WebXR sessions.", PrivacyImpact::High, "Same-origin only; prompts the user."},
// --- medium privacy impact ---
{"accelerometer", "Device motion sensor — can fingerprint and infer behaviour.", PrivacyImpact::Medium, "Same-origin only."},
{"ambient-light-sensor", "Reads ambient light level around the device.", PrivacyImpact::Medium, "Same-origin only."},
{"battery", "Battery Status API — a classic fingerprinting vector.", PrivacyImpact::Medium, "Same-origin only."},
{"gyroscope", "Device orientation sensor — fingerprinting and behaviour inference.", PrivacyImpact::Medium, "Same-origin only."},
{"magnetometer", "Compass readings — can leak details of the user's surroundings.", PrivacyImpact::Medium, "Same-origin only."},
{"keyboard-map", "Reads the physical keyboard layout — a small but real fingerprint.", PrivacyImpact::Medium, "Same-origin only."},
{"gamepad", "Enumerates connected controllers and their button state.", PrivacyImpact::Medium, "Same-origin only."},
{"midi", "Web MIDI — access to attached music hardware.", PrivacyImpact::Medium, "Same-origin only; prompts the user."},
{"payment", "Payment Request API — can expose stored payment handles.", PrivacyImpact::Medium, "Same-origin only."},
{"publickey-credentials-get", "WebAuthn credential requests.", PrivacyImpact::Medium, "Same-origin only."},
{"screen-wake-lock", "Keeps the screen awake — drains battery and signals intent.", PrivacyImpact::Medium, "Same-origin only."},
{"speaker-selection", "Enumerates and switches audio output devices.", PrivacyImpact::Medium, "Same-origin only."},
{"web-share", "Invokes the OS share sheet with chosen content.", PrivacyImpact::Medium, "Same-origin only."},
{"encrypted-media", "DRM (EME) — playback identity can be correlated.", PrivacyImpact::Medium, "Same-origin only."},
{"document-domain", "Let frames relax the same-origin policy via document.domain.", PrivacyImpact::Medium, "Allowed in same-origin pages; deprecated."},
// --- low privacy impact ---
{"autoplay", "Autoplay media with/without sound — not a data leak, an annoyance knob.", PrivacyImpact::Low, "Muted autoplay allowed; audible blocked."},
{"cross-origin-isolated", "COOP/COEP isolation for SharedArrayBuffer — hardens the page.", PrivacyImpact::Low, "Not isolated."},
{"fullscreen", "Element.requestFullscreen().", PrivacyImpact::Low, "Same-origin only."},
{"navigation-override", "Lets a frame intercept its own top-level navigations.", PrivacyImpact::Low, "Disabled."},
{"picture-in-picture", "Floating always-on-top video window.", PrivacyImpact::Low, "Same-origin only."},
};
return FEATURES;
}
// --- small string helpers ----------------------------------------------------
static std::string toLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
static std::string trim(const std::string& s) {
size_t b = s.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return "";
size_t e = s.find_last_not_of(" \t\r\n");
return s.substr(b, e - b + 1);
}
static std::vector<std::string> splitOn(const std::string& s, char sep) {
std::vector<std::string> out;
std::string cur;
for (char c : s) {
if (c == sep) {
out.push_back(cur);
cur.clear();
} else {
cur += c;
}
}
out.push_back(cur);
return out;
}
/// Split on runs of whitespace (std::regex's \s+ equivalent).
static std::vector<std::string> splitOnSpaces(const std::string& s) {
std::vector<std::string> out;
std::string cur;
for (char c : s) {
if (std::isspace(static_cast<unsigned char>(c))) {
if (!cur.empty()) out.push_back(cur);
cur.clear();
} else {
cur += c;
}
}
if (!cur.empty()) out.push_back(cur);
return out;
}
static bool startsWith(const std::string& s, const std::string& prefix) {
return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
}
/// ^(https?://|https?:)[\w.-]+(:\d+)?$ — a plausible origin token.
static bool isOriginLike(const std::string& raw) {
const std::string token = toLower(raw);
size_t pos = 0;
if (startsWith(token, "https://")) pos = 8;
else if (startsWith(token, "http://")) pos = 7;
else if (startsWith(token, "https:")) pos = 6;
else if (startsWith(token, "http:")) pos = 5;
else return false;
const size_t hostStart = pos;
while (pos < token.size() &&
(std::isalnum(static_cast<unsigned char>(token[pos])) || token[pos] == '.' ||
token[pos] == '_' || token[pos] == '-')) {
pos++;
}
if (pos == hostStart) return false; // empty host
if (pos == token.size()) return true;
// optional :port, and nothing else
if (token[pos] != ':') return false;
const std::string port = token.substr(pos + 1);
return !port.empty() &&
std::all_of(port.begin(), port.end(),
[](unsigned char c) { return std::isdigit(c) != 0; });
}
/// ^[a-z][a-z0-9-]*$
static bool isFeatureName(const std::string& name) {
if (name.empty() || !std::islower(static_cast<unsigned char>(name[0]))) return false;
return std::all_of(name.begin() + 1, name.end(), [](unsigned char c) {
return std::islower(c) != 0 || std::isdigit(c) != 0 || c == '-';
});
}
/// Strip one pair of surrounding double quotes, the TS `replace(/^"(.*)"$/, '$1')`.
static std::string unquote(const std::string& t) {
if (t.size() >= 2 && t.front() == '"' && t.back() == '"') return t.substr(1, t.size() - 2);
return t;
}
// --- build / parse / validate -------------------------------------------------
static std::string formatAllowlist(const std::vector<std::string>& tokens) {
if (tokens.size() == 1 && tokens[0] == "*") return "*";
std::string out = "(";
for (size_t i = 0; i < tokens.size(); i++) {
if (i > 0) out += ' ';
out += tokens[i];
}
return out + ")";
}
/** Assemble a Permissions-Policy header value from a feature map. */
std::string buildPermissionsPolicy(const FeatureMap& featuresMap) {
// std::map iterates in key order — the TS Object.keys().sort() equivalent.
std::string out;
for (const auto& [name, tokens] : featuresMap) {
if (!out.empty()) out += ", ";
out += name + "=" + formatAllowlist(tokens);
}
return out;
}
/** Strip an optional "Permissions-Policy:" prefix (case-insensitive). */
static std::string stripHeaderName(const std::string& header) {
std::string cleaned = trim(header);
static const std::string kPrefixLower = "permissions-policy:";
const std::string lower = toLower(cleaned);
if (startsWith(lower, kPrefixLower)) {
cleaned = trim(cleaned.substr(kPrefixLower.size()));
}
return cleaned;
}
/**
* Parse a Permissions-Policy header value back into a feature map.
* Returns std::nullopt when the syntax is malformed (bad directive, missing
* allowlist, bad token).
*/
std::optional<FeatureMap> parsePermissionsPolicy(const std::string& header) {
const std::string cleaned = stripHeaderName(header);
if (cleaned.empty()) return std::nullopt;
FeatureMap map;
for (const std::string& rawDirective : splitOn(cleaned, ',')) {
const std::string directive = trim(rawDirective);
if (directive.empty()) return std::nullopt;
const size_t eq = directive.find('=');
if (eq == std::string::npos || eq == 0) return std::nullopt;
const std::string name = trim(directive.substr(0, eq));
const std::string value = trim(directive.substr(eq + 1));
if (!isFeatureName(name)) return std::nullopt;
if (value == "*" || value == "self") {
map[name] = {value};
continue;
}
if (value.size() < 2 || value.front() != '(' || value.back() != ')') return std::nullopt;
const std::string inner = trim(value.substr(1, value.size() - 2));
if (inner.empty()) {
map[name] = {};
continue;
}
std::vector<std::string> tokens;
for (const std::string& raw : splitOnSpaces(inner)) tokens.push_back(unquote(raw));
for (const std::string& token : tokens) {
if (token != "self" && token != "*" && !isOriginLike(token)) return std::nullopt;
}
map[name] = std::move(tokens);
}
return map;
}
struct Validation {
bool valid = false;
std::vector<std::string> errors;
};
/** Syntax-check a header the same way parsePermissionsPolicy does, with messages. */
Validation validatePermissionsPolicy(const std::string& header) {
Validation result;
const std::string cleaned = stripHeaderName(header);
if (cleaned.empty()) {
result.errors.push_back("Header is empty.");
return result;
}
const std::vector<std::string> directives = splitOn(cleaned, ',');
for (size_t i = 0; i < directives.size(); i++) {
const std::string directive = trim(directives[i]);
const std::string where = "Directive " + std::to_string(i + 1);
if (directive.empty()) {
result.errors.push_back(where + ": empty (stray comma?).");
continue;
}
const size_t eq = directive.find('=');
if (eq == std::string::npos || eq == 0) {
result.errors.push_back(where + ": expected `feature=allowlist`, got `" + directive + "`.");
continue;
}
const std::string name = trim(directive.substr(0, eq));
const std::string value = trim(directive.substr(eq + 1));
if (!isFeatureName(name)) {
result.errors.push_back(where + ": `" + name + "` is not a valid feature name.");
continue;
}
if (value == "*" || value == "self") continue;
if (value.size() < 2 || value.front() != '(' || value.back() != ')') {
result.errors.push_back(where + ": `" + name + "` allowlist must be `*`, `self`, or `(...)` — got `" + value + "`.");
continue;
}
const std::string inner = trim(value.substr(1, value.size() - 2));
if (inner.empty()) continue; // () = disabled, valid
for (const std::string& raw : splitOnSpaces(inner)) {
const std::string token = unquote(raw);
if (token != "self" && token != "*" && !isOriginLike(token)) {
result.errors.push_back(where + ": `" + name + "` has an invalid allowlist token `" + raw + "`.");
}
}
}
result.valid = result.errors.empty();
return result;
}
/**
* Privacy score 0-100 for a policy: how much it locks down the catalog.
* Each feature is weighted by privacy impact (high 3, medium 2, low 1).
* Disabled `()` earns full credit, `self` or an origin list half, `*` or
* "absent from the policy" none (the browser default stays in force).
*/
int privacyScore(const FeatureMap& featuresMap) {
static const int weightByImpact[] = {3, 2, 1}; // indexed by PrivacyImpact (High, Medium, Low)
double earned = 0;
double possible = 0;
for (const FeatureInfo& feature : features()) {
const int w = weightByImpact[static_cast<int>(feature.privacyImpact)];
possible += w;
const auto it = featuresMap.find(feature.name);
if (it == featuresMap.end()) continue;
const std::vector<std::string>& allowlist = it->second;
if (allowlist.empty()) earned += w; // () disabled
else if (!(allowlist.size() == 1 && allowlist[0] == "*")) earned += w / 2.0; // self / origins
}
return static_cast<int>(std::lround((earned / possible) * 100.0));
}
} // namespace perms_policy
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →