pmcp 2.22.1

High-quality Rust SDK for Model Context Protocol (MCP) with full TypeScript SDK compatibility
Documentation
# Count the entries in a cargo-deny config's `[bans].allow` array.
#
# Usage:
#   awk -f scripts/deny-allow-entry-count.awk crates/pmcp-package/deny.toml
#
# Prints exactly one non-negative integer on stdout:
#
#   N >= 1  the `[bans]` section has an `allow` array holding N entries
#   0       the file has no `[bans]` section, OR `[bans]` has no `allow` array,
#           OR that array is empty
#
# All three zero cases are the SAME verdict for the caller — the allowlist is
# not providing deny-by-default protection — so the caller's error message names
# all three possibilities rather than guessing which one occurred.
#
# ---------------------------------------------------------------------------
# Why this is a checked-in awk program and not a grep in the Makefile recipe
# ---------------------------------------------------------------------------
# A non-empty `[bans].allow` list is what makes cargo-deny deny-by-default. An
# EMPTIED list is therefore a complete, silent bypass: cargo-deny reports
# "bans ok" for `allow = []` exactly as vacuously as it does for a missing
# config file. The gate must be able to tell those apart from a healthy config.
#
# Two line-oriented spellings were proposed and BOTH are broken, which is why
# this is a parser (cross-AI review, Phase 122, MEDIUM):
#
#   grep 'allow = \['     ALSO MATCHES `allow = []`. This is the precise bypass
#                         the guard exists to prevent, so the naive guard passes
#                         exactly when it must fail.
#   grep '{ name ='       ALSO MATCHES entries in other sections. It is not a
#                         hypothetical: cargo-deny configs in this repo carry a
#                         `[licenses]` stanza with its OWN `allow` key, so a
#                         file-wide count reads the wrong section.
#
# Being a separate file makes it separately testable, exactly as
# `scripts/named-test-binary-count.awk` is. `make no-crypto-allowlist-guard-selftest`
# feeds it six fixtures and is a declared PREREQUISITE of the gate that reads
# it, so the gate and the proof of the gate cannot drift.
#
# ---------------------------------------------------------------------------
# The three rules that make the reading trustworthy
# ---------------------------------------------------------------------------
# 1. COMMENTS ARE STRIPPED BEFORE MATCHING. `crates/pmcp-package/deny.toml`'s
#    header prose explains this very guard, and in doing so mentions `allow` and
#    writes `{ name = ... }` inline. A comment-blind counter would count the
#    DOCUMENTATION that describes the check — a self-invalidating reading that
#    would report a healthy allowlist for a file whose array was empty. Stripping
#    is quote-aware so a `#` inside a crate-name string is not treated as a
#    comment start.
#
# 2. COUNTING IS SECTION-SCOPED TO `[bans]`. Only `[bans]` is in scope;
#    `[licenses]`, `[advisories]`, `[graph]` and `[sources]` are explicitly out.
#    The scoping works in BOTH directions — a `[licenses] allow = []` appearing
#    before or after the `[bans]` stanza changes nothing, because the count is
#    keyed on the section active at the time and is never reset by a later
#    section.
#
# 3. ARRAY STATE IS TRACKED BY BRACKET DEPTH, so a single-line
#    `allow = [ { name = "x" } ]` and a multi-line array are both handled. The
#    opening line is itself counted before the depth check, which is what makes
#    the single-line form work.
#
# ---------------------------------------------------------------------------
# Blind spot, stated (repo convention: a mechanism names what it cannot see)
# ---------------------------------------------------------------------------
# This counts SYNTACTIC entries. It does NOT validate that each named crate
# exists, that the names are unique, that they are spelled correctly, or that
# they correspond to anything in the resolved dependency graph. A list of five
# misspelled names reads as 5 here — and would then fail the actual cargo-deny
# run, which is the check that has the real answer. This program's only job is
# to prove the allowlist is non-empty so that cargo-deny's "ok" is meaningful.

# Remove a TOML comment from a line, respecting double-quoted strings.
# A line whose first non-whitespace character is `#` reduces to the empty string.
function strip_comment(s,   out, i, c, inq) {
    out = ""
    inq = 0
    for (i = 1; i <= length(s); i++) {
        c = substr(s, i, 1)
        if (c == "\"") {
            inq = !inq
            out = out c
            continue
        }
        if (c == "#" && !inq) {
            break
        }
        out = out c
    }
    return out
}

# Count `{ name = ` / `{ crate = ` entry openers on a line. cargo-deny 0.18.3
# accepts both key spellings, so both are counted.
function count_entries(s,   tmp) {
    tmp = s
    return gsub(/\{[[:space:]]*(name|crate)[[:space:]]*=/, "", tmp)
}

# Net bracket depth change contributed by a line.
function net_brackets(s,   tmp, opens, closes) {
    tmp = s
    opens = gsub(/\[/, "", tmp)
    tmp = s
    closes = gsub(/\]/, "", tmp)
    return opens - closes
}

{
    line = strip_comment($0)

    if (in_array) {
        count += count_entries(line)
        depth += net_brackets(line)
        if (depth <= 0) {
            in_array = 0
        }
        next
    }

    # Section header: `[bans]`, `[licenses]`, `[graph]`, ...
    if (line ~ /^[[:space:]]*\[[A-Za-z0-9_.-]+\]/) {
        section = line
        sub(/^[[:space:]]*\[/, "", section)
        sub(/\].*$/, "", section)
        next
    }

    # Only `[bans].allow` opens a counted array.
    if (section == "bans" && line ~ /^[[:space:]]*allow[[:space:]]*=[[:space:]]*\[/) {
        in_array = 1
        depth = 0
        count += count_entries(line)
        depth += net_brackets(line)
        if (depth <= 0) {
            in_array = 0
        }
    }
}

END { print count + 0 }