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 — Permissions-Policy header builder, parser, validator and
* privacy scorer.
*
* Language: C (C11, standard library only)
* Source: CosmoDev polyglot showcase port of the Permissions-Policy Builder
* tool, ported from src/lib/permissions-policy.ts (the canonical
* TypeScript implementation).
* License: 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 here as a list of feature -> allowlist entries:
* camera=() -> 0 tokens (disabled)
* microphone=* -> ["*"] (every origin)
* geolocation=(self) -> ["self"]
* usb=(https://a.example ...) -> origin list
* Features absent from the map are absent from the header (browser default).
*
* The TS reference leans on regexes; C has none in its standard library, so the
* two token grammars are hand-written predicates below — same accepted language,
* no dependency.
*
* Build: cc -std=c11 permissions-policy.c
*/
#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strncasecmp (POSIX) */
/* --------------------------------------------------------------- catalogue --- */
typedef enum {
IMPACT_HIGH,
IMPACT_MEDIUM,
IMPACT_LOW
} privacy_impact;
typedef struct {
/* The directive token used in the header, e.g. `geolocation`. */
const char *name;
const char *description;
privacy_impact impact;
/* What browsers do when the feature is absent from the policy. */
const char *default_browser_behavior;
} feature_info;
static const feature_info FEATURES[] = {
/* --- high privacy impact ------------------------------------------------- */
{ "camera", "Access the device camera for photos / video calls.", IMPACT_HIGH, "Same-origin only; prompts the user." },
{ "microphone", "Capture audio from the device microphone.", IMPACT_HIGH, "Same-origin only; prompts the user." },
{ "geolocation", "Read the precise GPS location of the visitor.", IMPACT_HIGH, "Same-origin only; prompts the user." },
{ "display-capture", "Screen / window sharing via getDisplayMedia.", IMPACT_HIGH, "Same-origin only; prompts the user." },
{ "idle-detection", "Detects when the user is away from the device - reveals usage patterns.", IMPACT_HIGH, "Disabled; prompts the user." },
{ "serial", "Talk to serial devices (Arduinos, POS terminals) over a physical port.", IMPACT_HIGH, "Disabled; prompts the user." },
{ "usb", "WebUSB - direct access to connected USB devices.", IMPACT_HIGH, "Disabled; prompts the user." },
{ "hid", "Human Interface Devices - raw access to unusual keyboards, gamepads, sensors.", IMPACT_HIGH, "Disabled; prompts the user." },
{ "xr-spatial-tracking", "Tracks head / hand position in WebXR sessions.", IMPACT_HIGH, "Same-origin only; prompts the user." },
/* --- medium privacy impact ----------------------------------------------- */
{ "accelerometer", "Device motion sensor - can fingerprint and infer behaviour.", IMPACT_MEDIUM, "Same-origin only." },
{ "ambient-light-sensor", "Reads ambient light level around the device.", IMPACT_MEDIUM, "Same-origin only." },
{ "battery", "Battery Status API - a classic fingerprinting vector.", IMPACT_MEDIUM, "Same-origin only." },
{ "gyroscope", "Device orientation sensor - fingerprinting and behaviour inference.", IMPACT_MEDIUM, "Same-origin only." },
{ "magnetometer", "Compass readings - can leak details of the user's surroundings.", IMPACT_MEDIUM, "Same-origin only." },
{ "keyboard-map", "Reads the physical keyboard layout - a small but real fingerprint.", IMPACT_MEDIUM, "Same-origin only." },
{ "gamepad", "Enumerates connected controllers and their button state.", IMPACT_MEDIUM, "Same-origin only." },
{ "midi", "Web MIDI - access to attached music hardware.", IMPACT_MEDIUM, "Same-origin only; prompts the user." },
{ "payment", "Payment Request API - can expose stored payment handles.", IMPACT_MEDIUM, "Same-origin only." },
{ "publickey-credentials-get", "WebAuthn credential requests.", IMPACT_MEDIUM, "Same-origin only." },
{ "screen-wake-lock", "Keeps the screen awake - drains battery and signals intent.", IMPACT_MEDIUM, "Same-origin only." },
{ "speaker-selection", "Enumerates and switches audio output devices.", IMPACT_MEDIUM, "Same-origin only." },
{ "web-share", "Invokes the OS share sheet with chosen content.", IMPACT_MEDIUM, "Same-origin only." },
{ "encrypted-media", "DRM (EME) - playback identity can be correlated.", IMPACT_MEDIUM, "Same-origin only." },
{ "document-domain", "Let frames relax the same-origin policy via document.domain.", IMPACT_MEDIUM, "Allowed in same-origin pages; deprecated." },
/* --- low privacy impact -------------------------------------------------- */
{ "autoplay", "Autoplay media with/without sound - not a data leak, an annoyance knob.", IMPACT_LOW, "Muted autoplay allowed; audible blocked." },
{ "cross-origin-isolated", "COOP/COEP isolation for SharedArrayBuffer - hardens the page.", IMPACT_LOW, "Not isolated." },
{ "fullscreen", "Element.requestFullscreen().", IMPACT_LOW, "Same-origin only." },
{ "navigation-override", "Lets a frame intercept its own top-level navigations.", IMPACT_LOW, "Disabled." },
{ "picture-in-picture", "Floating always-on-top video window.", IMPACT_LOW, "Same-origin only." },
};
static const size_t FEATURE_COUNT = sizeof FEATURES / sizeof FEATURES[0];
/* --------------------------------------------------------------- policy map --- */
enum {
MAX_TOKENS = 16, /* allowlist origins per directive */
MAX_DIRECTIVES = 64, /* directives per policy */
MAX_TOKEN_LEN = 128,
MAX_NAME_LEN = 64,
MAX_ERRORS = 32,
MAX_ERROR_LEN = 200
};
/* One `feature=allowlist` pair. `token_count == 0` means `()`, i.e. disabled. */
typedef struct {
char name[MAX_NAME_LEN];
char tokens[MAX_TOKENS][MAX_TOKEN_LEN];
size_t token_count;
} policy_entry;
/* feature -> allowlist. Entries are kept in insertion order; the builder sorts. */
typedef struct {
policy_entry entries[MAX_DIRECTIVES];
size_t count;
} feature_map;
typedef struct {
bool valid;
char errors[MAX_ERRORS][MAX_ERROR_LEN];
size_t error_count;
} validation_result;
/* Append a feature with its allowlist. `tokens` may be NULL when count is 0. */
bool feature_map_set(feature_map *map, const char *name,
const char *const *tokens, size_t token_count)
{
policy_entry *entry;
if (map == NULL || map->count >= MAX_DIRECTIVES || token_count > MAX_TOKENS) {
return false;
}
entry = &map->entries[map->count++];
snprintf(entry->name, sizeof entry->name, "%s", name);
entry->token_count = token_count;
for (size_t i = 0; i < token_count; i++) {
snprintf(entry->tokens[i], MAX_TOKEN_LEN, "%s", tokens[i]);
}
return true;
}
/* Look up a feature's allowlist, or NULL when it is absent from the policy. */
const policy_entry *feature_map_get(const feature_map *map, const char *name)
{
if (map == NULL) {
return NULL;
}
for (size_t i = 0; i < map->count; i++) {
if (strcmp(map->entries[i].name, name) == 0) {
return &map->entries[i];
}
}
return NULL;
}
/* ------------------------------------------------------------ token grammar --- */
/* FEATURE_NAME_RE: /^[a-z][a-z0-9-]*$/ */
static bool is_feature_name(const char *s)
{
if (s == NULL || !islower((unsigned char) s[0])) {
return false;
}
for (const char *p = s + 1; *p != '\0'; p++) {
if (!islower((unsigned char) *p) && !isdigit((unsigned char) *p) && *p != '-') {
return false;
}
}
return true;
}
/* ORIGIN_RE: /^(https?:\/\/|https?:)[\w.-]+(:\d+)?$/i
* i.e. an http/https scheme (with or without "//"), a host of word characters,
* dots and dashes, and an optional numeric port. */
static bool is_origin(const char *s)
{
const char *p = s;
size_t host_len = 0;
if (s == NULL) {
return false;
}
if (strncasecmp(p, "https:", 6) == 0) {
p += 6;
} else if (strncasecmp(p, "http:", 5) == 0) {
p += 5;
} else {
return false;
}
if (strncmp(p, "//", 2) == 0) {
p += 2;
}
/* Host: one or more of [A-Za-z0-9_.-]. */
while (*p != '\0' && (isalnum((unsigned char) *p) || *p == '_' || *p == '.' || *p == '-')) {
p++;
host_len++;
}
if (host_len == 0) {
return false;
}
/* Optional :port, digits only. */
if (*p == ':') {
p++;
if (!isdigit((unsigned char) *p)) {
return false;
}
while (isdigit((unsigned char) *p)) {
p++;
}
}
return *p == '\0';
}
/* An allowlist token is `self`, `*`, or a valid origin. */
static bool is_allowlist_token(const char *token)
{
return strcmp(token, "self") == 0 || strcmp(token, "*") == 0 || is_origin(token);
}
/* ------------------------------------------------------------ string helpers --- */
/* Trim ASCII whitespace from both ends, in place. */
static char *trim(char *s)
{
char *end;
while (*s != '\0' && isspace((unsigned char) *s)) {
s++;
}
end = s + strlen(s);
while (end > s && isspace((unsigned char) end[-1])) {
end--;
}
*end = '\0';
return s;
}
/* Strip one layer of surrounding double quotes, as /^"(.*)"$/ does. */
static void unquote(char *s)
{
size_t len = strlen(s);
if (len >= 2 && s[0] == '"' && s[len - 1] == '"') {
memmove(s, s + 1, len - 2);
s[len - 2] = '\0';
}
}
/* Drop an optional leading `Permissions-Policy:` (case-insensitive) and trim.
* Returns a pointer into `buf`, which receives a mutable copy of `header`. */
static char *strip_header_prefix(const char *header, char *buf, size_t buf_size)
{
char *s;
snprintf(buf, buf_size, "%s", (header != NULL) ? header : "");
s = trim(buf);
if (strncasecmp(s, "permissions-policy", 18) == 0) {
char *p = s + 18;
while (*p != '\0' && isspace((unsigned char) *p)) {
p++;
}
if (*p == ':') {
p++;
while (*p != '\0' && isspace((unsigned char) *p)) {
p++;
}
s = p;
}
}
return s;
}
/* ------------------------------------------------------------------- build --- */
static int compare_entries(const void *a, const void *b)
{
return strcmp(((const policy_entry *) a)->name, ((const policy_entry *) b)->name);
}
/*
* Assemble a Permissions-Policy header value from a feature map. Directives are
* emitted in sorted order so the same policy always renders the same string.
* Writes into `out` and returns it.
*/
char *build_permissions_policy(const feature_map *map, char *out, size_t out_size)
{
policy_entry sorted[MAX_DIRECTIVES];
size_t written = 0;
out[0] = '\0';
if (map == NULL || map->count == 0) {
return out;
}
memcpy(sorted, map->entries, map->count * sizeof sorted[0]);
qsort(sorted, map->count, sizeof sorted[0], compare_entries);
for (size_t i = 0; i < map->count && written + 1 < out_size; i++) {
const policy_entry *e = &sorted[i];
int n;
if (i > 0) {
n = snprintf(out + written, out_size - written, ", ");
if (n < 0) break;
written += (size_t) n;
}
/* A lone `*` is written bare; everything else is parenthesised, so an
* empty allowlist renders as `()` — the "disabled" form. */
if (e->token_count == 1 && strcmp(e->tokens[0], "*") == 0) {
n = snprintf(out + written, out_size - written, "%s=*", e->name);
if (n < 0) break;
written += (size_t) n;
continue;
}
n = snprintf(out + written, out_size - written, "%s=(", e->name);
if (n < 0) break;
written += (size_t) n;
for (size_t t = 0; t < e->token_count && written + 1 < out_size; t++) {
n = snprintf(out + written, out_size - written, "%s%s",
(t > 0) ? " " : "", e->tokens[t]);
if (n < 0) break;
written += (size_t) n;
}
if (written + 1 < out_size) {
n = snprintf(out + written, out_size - written, ")");
if (n < 0) break;
written += (size_t) n;
}
}
return out;
}
/* ------------------------------------------------------------------- parse --- */
/*
* Parse a Permissions-Policy header value into a feature map. Accepts an
* optional `Permissions-Policy:` prefix. Returns false when the syntax is
* malformed (bad directive, missing allowlist, bad token).
*/
bool parse_permissions_policy(const char *header, feature_map *out)
{
char buf[4096];
char *cleaned, *directive, *save = NULL;
if (out == NULL) {
return false;
}
out->count = 0;
cleaned = strip_header_prefix(header, buf, sizeof buf);
if (cleaned[0] == '\0') {
return false;
}
for (directive = strtok_r(cleaned, ",", &save);
directive != NULL;
directive = strtok_r(NULL, ",", &save)) {
char *name, *value, *eq;
const char *tokens[MAX_TOKENS];
char token_store[MAX_TOKENS][MAX_TOKEN_LEN];
size_t token_count = 0;
directive = trim(directive);
if (directive[0] == '\0') {
return false; /* stray comma */
}
eq = strchr(directive, '=');
if (eq == NULL || eq == directive) {
return false; /* not `feature=allowlist` */
}
*eq = '\0';
name = trim(directive);
value = trim(eq + 1);
if (!is_feature_name(name)) {
return false;
}
/* `*` and bare `self` are shorthands for a one-token allowlist. */
if (strcmp(value, "*") == 0 || strcmp(value, "self") == 0) {
const char *one[1] = { value };
if (!feature_map_set(out, name, one, 1)) {
return false;
}
continue;
}
{
size_t vlen = strlen(value);
if (vlen < 2 || value[0] != '(' || value[vlen - 1] != ')') {
return false;
}
value[vlen - 1] = '\0';
value = trim(value + 1);
}
if (value[0] == '\0') {
/* `()` = the feature is disabled outright. */
if (!feature_map_set(out, name, NULL, 0)) {
return false;
}
continue;
}
{
char *token, *tsave = NULL;
for (token = strtok_r(value, " \t\r\n", &tsave);
token != NULL;
token = strtok_r(NULL, " \t\r\n", &tsave)) {
if (token_count >= MAX_TOKENS) {
return false;
}
snprintf(token_store[token_count], MAX_TOKEN_LEN, "%s", token);
unquote(token_store[token_count]);
if (!is_allowlist_token(token_store[token_count])) {
return false;
}
tokens[token_count] = token_store[token_count];
token_count++;
}
}
if (!feature_map_set(out, name, tokens, token_count)) {
return false;
}
}
return true;
}
/* ---------------------------------------------------------------- validate --- */
static void push_error(validation_result *result, const char *fmt, ...)
{
va_list args;
if (result->error_count >= MAX_ERRORS) {
return;
}
va_start(args, fmt);
vsnprintf(result->errors[result->error_count], MAX_ERROR_LEN, fmt, args);
va_end(args);
result->error_count++;
}
/* Syntax-check a header the same way parse_permissions_policy() does, but
* collecting a message per problem instead of bailing at the first. */
validation_result validate_permissions_policy(const char *header)
{
validation_result result = { true, {{0}}, 0 };
char buf[4096];
char *cleaned, *directive, *save = NULL;
int index = 0;
cleaned = strip_header_prefix(header, buf, sizeof buf);
if (cleaned[0] == '\0') {
push_error(&result, "Header is empty.");
result.valid = false;
return result;
}
for (directive = strtok_r(cleaned, ",", &save);
directive != NULL;
directive = strtok_r(NULL, ",", &save), index++) {
char *name, *value, *eq;
char where[32];
snprintf(where, sizeof where, "Directive %d", index + 1);
directive = trim(directive);
if (directive[0] == '\0') {
push_error(&result, "%s: empty (stray comma?).", where);
continue;
}
eq = strchr(directive, '=');
if (eq == NULL || eq == directive) {
push_error(&result, "%s: expected `feature=allowlist`, got `%s`.", where, directive);
continue;
}
*eq = '\0';
name = trim(directive);
value = trim(eq + 1);
if (!is_feature_name(name)) {
push_error(&result, "%s: `%s` is not a valid feature name.", where, name);
continue;
}
if (strcmp(value, "*") == 0 || strcmp(value, "self") == 0) {
continue;
}
{
size_t vlen = strlen(value);
if (vlen < 2 || value[0] != '(' || value[vlen - 1] != ')') {
push_error(&result,
"%s: `%s` allowlist must be `*`, `self`, or `(...)` - got `%s`.",
where, name, value);
continue;
}
value[vlen - 1] = '\0';
value = trim(value + 1);
}
if (value[0] == '\0') {
continue; /* () = disabled, valid */
}
{
char *raw, *tsave = NULL;
for (raw = strtok_r(value, " \t\r\n", &tsave);
raw != NULL;
raw = strtok_r(NULL, " \t\r\n", &tsave)) {
char token[MAX_TOKEN_LEN];
snprintf(token, sizeof token, "%s", raw);
unquote(token);
if (!is_allowlist_token(token)) {
push_error(&result, "%s: `%s` has an invalid allowlist token `%s`.",
where, name, raw);
}
}
}
}
result.valid = result.error_count == 0;
return result;
}
/* ------------------------------------------------------------------- score --- */
/*
* Privacy score 0-100: how much of the catalogue the policy locks down. Each
* feature is weighted by privacy impact (high 3, medium 2, low 1). Disabled
* `()` earns full credit, `self` or an origin list half, and `*` or "absent
* from the policy" none (the browser default stays in force).
*/
int privacy_score(const feature_map *map)
{
double earned = 0.0, possible = 0.0;
for (size_t i = 0; i < FEATURE_COUNT; i++) {
const feature_info *feature = &FEATURES[i];
const policy_entry *entry;
double w;
switch (feature->impact) {
case IMPACT_HIGH: w = 3.0; break;
case IMPACT_MEDIUM: w = 2.0; break;
default: w = 1.0; break;
}
possible += w;
entry = feature_map_get(map, feature->name);
if (entry == NULL) {
continue; /* browser default — no credit */
}
if (entry->token_count == 0) {
earned += w; /* () disabled */
} else if (!(entry->token_count == 1 && strcmp(entry->tokens[0], "*") == 0)) {
earned += w / 2.0; /* self / origin list */
}
}
if (possible == 0.0) {
return 0;
}
/* JS Math.round: halves go up. */
return (int) ((earned / possible) * 100.0 + 0.5);
}
/* -------------------------------------------------------------------- demo --- */
int main(void)
{
feature_map map = {0};
feature_map parsed = {0};
char header[1024];
validation_result check;
/* A locked-down starter policy. */
const char *self_only[1] = { "self" };
const char *everywhere[1] = { "*" };
const char *partners[2] = { "self", "https://widgets.example.com" };
feature_map_set(&map, "geolocation", self_only, 1);
feature_map_set(&map, "camera", NULL, 0); /* disabled */
feature_map_set(&map, "microphone", NULL, 0); /* disabled */
feature_map_set(&map, "usb", NULL, 0); /* disabled */
feature_map_set(&map, "autoplay", everywhere, 1); /* allowed everywhere */
feature_map_set(&map, "payment", partners, 2);
build_permissions_policy(&map, header, sizeof header);
printf("Permissions-Policy: %s\n\n", header);
printf("privacy score: %d/100\n\n", privacy_score(&map));
/* Round-trip: the built header parses back to an equivalent map. */
if (parse_permissions_policy(header, &parsed)) {
printf("parsed %zu directives, score %d/100\n\n", parsed.count, privacy_score(&parsed));
} else {
printf("parse failed\n\n");
}
/* Validation reports every problem, not just the first. */
check = validate_permissions_policy("geolocation=(self), camera, usb=(ftp://nope), =broken");
printf("valid: %s\n", check.valid ? "yes" : "no");
for (size_t i = 0; i < check.error_count; i++) {
printf(" - %s\n", check.errors[i]);
}
return EXIT_SUCCESS;
}
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 →