native_messaging 0.2.0

Cross-platform Rust native messaging host for browser extensions (Chrome & Firefox), with async helpers and manifest installer.
Documentation
# native_messaging

A batteries-included Rust crate for **browser Native Messaging**:

- Build a **Native Messaging host** that talks to your extension over **stdin/stdout**
- Install, verify, and remove the **native host manifest** for multiple browsers
- Safe framing + size caps + structured errors (`NmError`)
- Cross-platform: **Windows / macOS / Linux**

This crate aims to be the “it just works” choice for [native messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging).

---

## What is Native Messaging?

Native Messaging is how a browser extension talks to a local native process (your “host”).
The protocol is:

1. A 4-byte length prefix (`u32`, **native endianness**)
2. Followed by that many bytes of **UTF-8 JSON**

Your host reads from **stdin** and writes to **stdout**.

### Big gotchas (read this first)

- **Disconnect is normal:** when the extension disconnects or browser exits, stdin usually closes.
  Treat `NmError::Disconnected` as a normal shutdown.
- **Never log to stdout:** stdout is reserved for framed messages. Logging to stdout corrupts the stream.
  Log to **stderr** or a file.
- **Size limits are real:**
  - host → browser: **1 MiB** (enforced)
  - browser → host: **64 MiB** (enforced)

---

## Install

```bash
cargo add native_messaging
````

Tokio is required for the async host helpers (recommended). Use these features:

```toml
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "sync"] }
```

---

## Quickstart: robust async host loop (recommended)

This is the easiest correct way to run a host continuously and reply to messages.

```rust
use native_messaging::host::{event_loop, NmError, Sender};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct In {
    ping: String,
}

#[derive(Serialize)]
struct Out {
    pong: String,
}

#[tokio::main]
async fn main() -> Result<(), NmError> {
    event_loop(|raw: String, send: Sender| async move {
        let incoming: In = serde_json::from_str(&raw).map_err(NmError::DeserializeJson)?;
        send.send(&Out { pong: incoming.ping }).await?;
        Ok(())
    })
    .await
}
```

### Logging (important)

Do **not** use `println!()` in a host. It writes to stdout and breaks the protocol.
Use stderr:

```rust
eprintln!("host starting"); // ✅ safe
// println!("host starting"); // ❌ unsafe
```

---

## JS extension example (Chrome/Chromium)

This is what the extension side typically looks like:

```js
const port = chrome.runtime.connectNative("com.example.host");

port.onMessage.addListener((msg) => {
  console.log("native reply:", msg);
});

port.onDisconnect.addListener(() => {
  console.log("native disconnected:", chrome.runtime.lastError);
});

port.postMessage({ ping: "hello" });
```

---

## Pure framing (unit-test friendly)

You can test framing without stdin/stdout using an in-memory buffer:

```rust
use native_messaging::host::{encode_message, decode_message, MAX_FROM_BROWSER};
use serde_json::json;
use std::io::Cursor;

let msg = json!({"hello": "world"});
let frame = encode_message(&msg).unwrap();

let mut cur = Cursor::new(frame);
let raw = decode_message(&mut cur, MAX_FROM_BROWSER).unwrap();

let back: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(back, msg);
```

---

## One-shot read/write helpers (convenience)

These helpers read one message from stdin and write one reply to stdout.
For production hosts, prefer `event_loop`.

```rust
use native_messaging::{get_message, send_message};
use native_messaging::host::NmError;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct In { ping: String }

#[derive(Serialize)]
struct Out { pong: String }

#[tokio::main]
async fn main() -> Result<(), NmError> {
    let raw = get_message().await?;
    let incoming: In = serde_json::from_str(&raw).map_err(NmError::DeserializeJson)?;
    send_message(&Out { pong: incoming.ping }).await?;
    Ok(())
}
```

---

## Installing a manifest (config-driven browsers)

This crate includes an installer for writing/verifying/removing manifests for supported browsers.

**Browser allowlists differ by family:**

* Chromium-family uses `allowed_origins` like: `chrome-extension://<EXT_ID>/`
* Firefox-family uses `allowed_extensions` like: `your-addon@example.org`

```rust
use std::path::Path;
use native_messaging::{install, Scope};

let host_name = "com.example.host";
let description = "Example native messaging host";

// On macOS/Linux, this must be an absolute path.
let exe_path = Path::new("/absolute/path/to/host-binary");

// Chromium-family allow-list:
let allowed_origins = vec![
    "chrome-extension://your_extension_id/".to_string(),
];

// Firefox-family allow-list:
let allowed_extensions = vec![
    "your-addon@example.org".to_string(),
];

// Install for selected browsers by key:
let browsers = &["chrome", "firefox", "edge"];

install(
    host_name,
    description,
    exe_path,
    &allowed_origins,
    &allowed_extensions,
    browsers,
    Scope::User,
).unwrap();
```

### Verify installation

```rust
use native_messaging::{verify_installed, Scope};

let ok = verify_installed("com.example.host", None, Scope::User).unwrap();
assert!(ok);
```

### Remove a manifest

```rust
use native_messaging::{remove, Scope};

remove("com.example.host", &["chrome", "firefox", "edge"], Scope::User).unwrap();
```

---

## Troubleshooting

Native messaging failures are usually **manifest** issues, not code.

### “Specified native messaging host not found”

Check:

* The extension calls the exact same `host_name` you installed (case-sensitive).
* The manifest exists at the expected location (User vs System scope).
* The manifest JSON is valid.

### “Access to the specified native messaging host is forbidden”

Check:

* Chromium-family: `allowed_origins` contains exact `chrome-extension://<id>/`
* Firefox-family: `allowed_extensions` contains your addon ID

### “Native host has exited” / “Failed to start native messaging host”

Check:

* The manifest `path` points to a real executable.
* On macOS/Linux, manifest `path` is absolute.
* Your host does not log to stdout.
* Your host handles disconnect cleanly (EOF → `NmError::Disconnected`).

### Host prints weird JSON / extension can’t parse

This almost always means **stdout was corrupted** by logs.
Switch logging to stderr/file.

---

## API overview

Most users only need:

* Host:

  * `native_messaging::host::event_loop`
  * `native_messaging::host::Sender`
  * `native_messaging::host::NmError`
* Installer:

  * `native_messaging::install`
  * `native_messaging::verify_installed`
  * `native_messaging::remove`
  * `native_messaging::Scope`

---

## Notes for crate maintainers / contributors

### Run tests (including docs)

```bash
cargo test
cargo test --doc
```

### Clippy (strict)

```bash
cargo clippy -- -D warnings
```

---

## License

MIT