jmap-cid-types 0.4.1

JMAP Blob Content Identifiers extension data types (draft-atwood-jmap-cid-00)
Documentation
# jmap-cid-types

draft-atwood-jmap-cid-00 wire-format types for the `jmap-*` crate family.

The `urn:ietf:params:jmap:cid` JMAP capability plus the `sha256` typed
string shape used in blob upload responses and FileNode objects.

## What it is

When a server advertises `urn:ietf:params:jmap:cid`, blob upload
responses (RFC 8620 §6.1) carry a `sha256` field with the SHA-256
digest of the uploaded content, encoded as a lowercase hexadecimal
string of exactly 64 characters. When the JMAP FileNode extension is
also advertised, FileNode objects gain the same `sha256` property.

This crate carries the typed `sha256` wire shape ([`Sha256`]) plus
the matching parse-error type ([`Sha256DigestError`]). Wiring the
shape into the Blob upload response surface and Session capability
detection happens in `jmap-base-client`.

## Why a separate crate

CID is not tied to any single consumer extension. The `sha256` field
is referenced by `draft-atwood-jmap-chat-00` (which defers to the CID
document as the normative definition), and the spec is structured to
fold cleanly into a future RFC 8620 bis or
`draft-ietf-jmap-filenode`. Standing CID up as its own crate keeps
the dep graph honest and avoids forcing a `jmap-chat-*` or
`jmap-filenode-*` dependency on consumers that only want content
identifiers.

## What it's for

The typed wire shape here is consumed by `jmap-base-client` for the
`BlobUploadResponse.sha256` field and the `Session::supports_cid()`
helper (both landed in `jmap-base-client`), and by a future
`jmap-filenode-types` revision for the FileNode `sha256` property.
The CID document — capability URI `urn:ietf:params:jmap:cid`, draft
owned by Mark Atwood — is deliberately independent of any single
consumer extension because the same content-hash field feeds Blob
upload responses, FileNode integrity, and the JMAP Chat draft's
attachment references.

## How to use

```toml
[dependencies]
jmap-cid-types = "0.1"
```

Pulls in `serde` and `serde_json` at runtime. No async runtime,
no `jmap-types`, no `jmap-server`, no `jmap-base-client`. Parse
and validate a `sha256` digest from a wire string:

```rust
use jmap_cid_types::Sha256;

// 64 lowercase hex characters per draft-atwood-jmap-cid-00 §2.
let digest: Sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    .parse()?;
assert_eq!(digest.as_ref().len(), 64);
# Ok::<(), jmap_cid_types::Sha256DigestError>(())
```

Use `Sha256::from(&[u8; 32])` (or `Sha256::from([u8; 32])` for an
owned array) to build a digest from the raw 32-byte output of a
SHA-256 hash function (e.g. `sha2::Sha256`) without going through a
hex string. The capability URI `"urn:ietf:params:jmap:cid"` is
detected via the `Session.capabilities` map in `jmap-base-client`.

Convert a digest to and from its CIDv1 (`raw` codec, `sha2-256`
multihash), per the draft's informative §Mapping to CIDv1 appendix:

```rust
use jmap_cid_types::Sha256;

let digest: Sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    .parse()?;
let cid = digest.to_cidv1();
assert_eq!(cid, "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku");
assert_eq!(Sha256::from_cidv1(&cid)?, digest);
# Ok::<(), Box<dyn std::error::Error>>(())
```

`to_cidv1_bytes` / `from_cidv1_bytes` do the same for the 36-byte
binary CID. The reverse direction accepts only multibase `b`, CID
version 1, codec `raw` and `sha2-256`; anything else is a
`CidV1Error`.

Convert a digest to and from the padded base64 used by the RFC 9404
`Blob/get` `digest:sha-256` property, so the two can be compared
("convert encodings before comparing", draft §Relationship to JMAP
Blob Management Extensions):

```rust
use jmap_cid_types::Sha256;

let digest: Sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    .parse()?;
assert_eq!(digest.to_base64(), "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=");
assert_eq!(Sha256::from_base64("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")?, digest);
# Ok::<(), Box<dyn std::error::Error>>(())
```

`from_base64` accepts only canonical padded RFC 4648 §4 base64 that
decodes to 32 bytes; anything else is a `Sha256Base64Error`.
`to_bytes` returns the 32 raw digest bytes.

## How it works

- [`Sha256`] is a `String`-backed newtype with parse-time ABNF
  validation per the draft `sha256-value` rule
  (`64( %x30-39 / %x61-66 )`): exactly 64 characters, lowercase hex
  only. Validation runs on `from_hex`, `TryFrom<&str>`, `FromStr`,
  and the `Deserialize` impl.
- `From<[u8; 32]>` / `From<&[u8; 32]>` for `Sha256` build a digest
  infallibly by formatting the raw 32-byte SHA-256 output as
  lowercase hex; no parse step is needed because the output is
  always valid by construction. The input is the output of a hash
  function (e.g. `sha2::Sha256::digest(data).into()`) — this crate
  carries no hash computation.
- The error type [`Sha256DigestError`] is a single-tier enum
  (`WrongLength { got }` or `NonHexLowercase { at, byte }`) with
  `#[non_exhaustive]` at the type level and per-variant
  `#[non_exhaustive]` so variant additions and per-variant field
  additions both remain semver-additive. Diagnostics carry byte
  position so callers can point at the bad byte without re-indexing
  the input.
- `#[non_exhaustive]` on every public struct and enum, so additive
  spec evolution stays a non-breaking change.
- No async. `#[forbid(unsafe_code)]` at the crate root.
- Runtime dependencies limited to `serde`, `serde_json`
  (the latter for the `CidCapability.extra` Map<String, Value>
  surface per workspace extras-preservation policy) and `base64`
  (for the RFC 9404 digest encoding).

## Gotchas

- Blob upload-response and Session-capability wiring live in
  `jmap-base-client`, not here.
  `jmap_base_client::BlobUploadResponse.sha256:
  Option<jmap_cid_types::Sha256>` is the typed binding for the
  upload-response `sha256` field; `Session::supports_cid()` is the
  helper that checks the capability map. This crate is the
  wire-shape source of truth; the binding lives one layer up.
- CID and the RFC 9404 BLOBEXT `Blob/get` `digest:sha-256` request
  are deliberately separate mechanisms with different encodings
  (lowercase hex vs base64) and different access patterns
  (unconditional at upload vs on-demand via `Blob/get`). See the
  draft §2.3.

## References

- [draft-atwood-jmap-cid-00] — JMAP Blob Content Identifiers (normative
  for `Sha256` and the `urn:ietf:params:jmap:cid` capability)
- [RFC 8620] — JMAP Core (Blob upload §6.1 is the binding point)
- [RFC 4648] — base32 alphabet used by the CIDv1 string form and
  base64 alphabet used by the RFC 9404 digest form
- [RFC 9404] — JMAP Blob Management Extensions (`digest:sha-256`)

[draft-atwood-jmap-cid-00]: https://github.com/MarkAtwood/jmap-chat-spec
[RFC 8620]: https://www.rfc-editor.org/rfc/rfc8620
[RFC 9404]: https://www.rfc-editor.org/rfc/rfc9404
[RFC 4648]: https://www.rfc-editor.org/rfc/rfc4648