# crx-manifest-parser

[![crates.io](https://img.shields.io/crates/v/crx-manifest-parser.svg)](https://crates.io/crates/crx-manifest-parser)
[![docs](https://docs.rs/crx-manifest-parser/badge.svg)](https://docs.rs/crx-manifest-parser)

Parse a Chrome / Chromium **Manifest V3** `manifest.json` string into a
strongly-typed [`Manifest`](https://docs.rs/crx-manifest-parser) struct
exposing the fields an extension security scanner actually cares about:

- `name`, `version`, `description`, `manifest_version`
- `permissions` and `optional_permissions` (named API tokens)
- `host_permissions` (match-patterns granting site access)
- `content_scripts` (the script + match list — the biggest cross-origin
  exposure surface)

Pure Rust, **zero dependencies** (no `serde`, no `serde_json`), `#![forbid(unsafe_code)]`.
Ships its own minimal, fully-tested JSON value parser so a manifest can be
decoded without dragging a JSON library into the build graph.

This is the manifest-decode layer behind the [**zovo.one**](https://zovo.one/)
Chrome-extension privacy &amp; security scanner, which audits the permissions
requested by any installed extension and flags the ones that can read your
data across every site.

## Why parse the manifest

The `manifest.json` is the single source of truth for what an extension is
allowed to do. Two extensions that both "just change your new tab page" can
declare wildly different access: one asks for `storage`, the other for
`<all_urls>` + `cookies` + content scripts on every site. The manifest is the
first thing a privacy review opens — this crate turns it into a typed value
you can branch on, render, or feed to a permission auditor (see the
companion [`permission-auditor`](https://crates.io/crates/permission-auditor)
crate).

## Install

```toml
[dependencies]
crx-manifest-parser = "0.1"
```

## Quick start

```rust
use crx_manifest_parser::Manifest;

let json = r#"{
    "manifest_version": 3,
    "name": "My Extension",
    "version": "1.4.2",
    "permissions": ["activeTab", "storage"],
    "host_permissions": ["https://*.example.com/*"],
    "content_scripts": [
        { "matches": ["https://*.example.com/*"], "js": ["content.js"] }
    ]
}"#;

let manifest = Manifest::from_json(json).expect("valid manifest");
assert_eq!(manifest.name.as_deref(), Some("My Extension"));
assert_eq!(manifest.manifest_version, Some(3));
assert_eq!(manifest.permissions, ["activeTab", "storage"]);
assert_eq!(manifest.host_permissions, ["https://*.example.com/*"]);
assert_eq!(manifest.content_scripts.len(), 1);
assert!(manifest.is_mv3());

// The full set of grant tokens, ready for a permission auditor.
assert_eq!(manifest.all_grants(), ["activeTab", "storage", "https://*.example.com/*"]);
```

## API surface

| Item | Description |
| --- | --- |
| `Manifest` | Typed struct over the security-relevant fields. |
| `ContentScript` | One `content_scripts` entry (`matches`, `js`, `css`, ...). |
| `Manifest::from_json(s)` | Parse a manifest string into a `Manifest`. |
| `Manifest::from_json_value(&v)` | Build from an already-parsed `Json` value. |
| `Manifest::all_grants()` | `permissions` + `host_permissions` concatenated. |
| `Manifest::content_match_patterns()` | Deduped site match-patterns across all content scripts. |
| `Manifest::is_mv3()` / `is_mv2()` | Manifest-version predicates. |
| `Json`, `ParseError` | The bundled JSON value model + parse error. |

## Design choices

- **Zero dependencies.** No `serde`, no `serde_json`. The crate carries a
  small recursive-descent JSON parser so it works in any sandboxed or
  constrained build context.
- **Missing fields are tolerated.** Manifests routinely omit keys; a missing
  `permissions` array is an empty `Vec`, not an error. A parse error only
  fires for malformed JSON or a field with the wrong JSON type (e.g.
  `permissions` given as a string).
- **No `unsafe`.** `#![forbid(unsafe_code)]` — there is no unsafe anywhere
  in the crate.
- **UTF-8 and escape fidelity.** Strings are decoded with full
  `\uXXXX` / surrogate-pair / `\b \f \n \r \t` handling, and raw control
  characters are rejected (per RFC 8259) so a manifest can't smuggle
  unescaped control bytes.

## License

MIT.

## Links

- **zovo.one** — the Chrome-extension privacy &amp; security scanner that
  uses this parser: <https://zovo.one/>
- Source (zovo monorepo): <https://github.com/theluckystrike/zovo>
- Full API docs: <https://docs.rs/crx-manifest-parser>
