~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/atproto

atproto's primitives in Rust, small and free of I/O

0
0
main
849 lines · 30.7 KB Raw History
//! Values checked against lexicons: a record, an object, a method's
//! parameters or its input, against the def that describes it, as
//! atproto's reference implementation (`@atproto/lexicon`) checks one.
//!
//! [`Lexicons`] holds a set of lexicon documents and checks a value against
//! a def by its reference (`nsid` for its main, `nsid#name`), following
//! references across the documents. Every field type is checked: null,
//! boolean, integer, string, bytes, cid-link, blob, array, object, params,
//! ref, union, unknown; `required` and `nullable`; a string's length in
//! UTF-8 bytes and in graphemes, its format, `const` and `enum`; an
//! integer's range; an array's length; a blob's `accept` and `maxSize`.
//! `knownValues` are a hint, not a rule, and are not enforced. A token
//! names a value and is no field's type, so no value is one. The first
//! failure is answered, with the path to it: `input.roles[2].role`.
//!
//! Checked against the reference's verdicts on lexicon.js's cases and a
//! few more (`tests/reference/lexicon.json`), and the formats against
//! atproto's interop vectors. Where this departs from the reference:
//!
//! - An at-uri may name a permissioned space, `at://<did>/space/<type>/<skey>`,
//!   or a record in one, `…/<writer did>/<collection>/<rkey>`, which the
//!   reference does not know yet.
//! - A blob's `accept` and `maxSize` are enforced; the reference leaves
//!   them to whoever stores the blob.
//! - The formats are the spec's, as atproto's interop vectors hold them,
//!   where the reference is laxer for old records' sake: a `cid` string is
//!   never a version 0 CID, a `datetime` always has its seconds and a time
//!   zone, a `language` tag's primary subtag is lower case and repeats no
//!   variant or extension, and a `uri` is 8 KiB at most.

use crate::base32;
use crate::syntax::{is_did, is_handle, is_nsid, is_record_key, is_tid};
use base64::Engine;
use base64::engine::general_purpose::GeneralPurpose;
use base64::engine::{DecodePaddingMode, GeneralPurposeConfig};
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::fmt;
use unicode_segmentation::UnicodeSegmentation;

/// Why a value is not what its def describes: where, and what is wrong.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LexiconError {
    /// Where in the value: `input.roles[2].role`.
    pub path: String,
    /// What is wrong there: `must be a string`.
    pub message: String,
}

impl fmt::Display for LexiconError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} {}", self.path, self.message)
    }
}

impl std::error::Error for LexiconError {}

type Checked = Result<(), LexiconError>;

fn fail(path: &str, message: impl Into<String>) -> Checked {
    Err(LexiconError {
        path: path.to_string(),
        message: message.into(),
    })
}

/// A set of lexicon documents, by their NSIDs.
#[derive(Debug, Clone, Default)]
pub struct Lexicons {
    docs: HashMap<String, Value>,
}

/// Base64 as `$bytes` holds it: padded or not.
const BASE64: GeneralPurpose = GeneralPurpose::new(
    &base64::alphabet::STANDARD,
    GeneralPurposeConfig::new()
        .with_encode_padding(false)
        .with_decode_padding_mode(DecodePaddingMode::Indifferent),
);

impl Lexicons {
    /// The documents `docs`, each by its `id`; one without is left out.
    pub fn new(docs: impl IntoIterator<Item = Value>) -> Lexicons {
        let docs = docs.into_iter().filter_map(|doc| {
            let id = doc["id"].as_str()?.to_string();
            Some((id, doc))
        });
        Lexicons {
            docs: docs.collect(),
        }
    }

    /// The def `reference` names, from within the document `base`: `#name`,
    /// `nsid#name`, or `nsid` for its main; with the NSID it is in.
    fn resolve<'a>(&'a self, base: &str, reference: &str) -> Option<(String, &'a Value)> {
        let (nsid, name) = match reference.split_once('#') {
            Some(("", name)) => (base, name),
            Some((nsid, name)) => (nsid, name),
            None => (reference, "main"),
        };
        let def = self.docs.get(nsid)?.get("defs")?.get(name)?;
        Some((nsid.to_string(), def))
    }

    /// The def `reference` names: `nsid` for its main, or `nsid#name`.
    pub fn def(&self, reference: &str) -> Option<&Value> {
        self.resolve("", reference).map(|(_, def)| def)
    }

    /// The def, of one of `types`, that `reference` names.
    fn def_of(&self, reference: &str, types: &[&str], path: &str) -> Result<&Value, LexiconError> {
        match self.def(reference) {
            Some(def) if types.contains(&def["type"].as_str().unwrap_or_default()) => Ok(def),
            Some(_) => Err(LexiconError {
                path: path.to_string(),
                message: format!("{reference} is not a {}", types.join(" or ")),
            }),
            None => Err(LexiconError {
                path: path.to_string(),
                message: format!("no lexicon {reference}"),
            }),
        }
    }

    /// Checks `value` against the record or object `reference` names.
    pub fn validate(&self, reference: &str, value: &Value) -> Checked {
        let def = self.def_of(reference, &["record", "object"], "record")?;
        let nsid = nsid_of(reference);
        special(value, "record")?;
        match def["type"].as_str() {
            Some("record") => self.object(nsid, &def["record"], value, "record"),
            _ => self.object(nsid, def, value, "object"),
        }
    }

    /// Checks `params` against the parameters of the method `nsid`.
    pub fn validate_params(&self, nsid: &str, params: &Map<String, Value>) -> Checked {
        let types = ["query", "procedure", "subscription"];
        let def = &self.def_of(nsid, &types, "params")?["parameters"];
        let Some(properties) = def["properties"].as_object() else {
            return Ok(());
        };
        for (key, prop) in properties {
            let path = format!("params.{key}");
            match params.get(key) {
                Some(v) => {
                    special(v, &path)?;
                    self.field(nsid, prop, v, &path)?;
                }
                None if listed(def, "required", key) && !defaulted(prop) => {
                    fail("params", format!("must have the property \"{key}\""))?;
                }
                None => {}
            }
        }
        Ok(())
    }

    /// Checks `input` against the JSON input of the procedure `nsid`.
    pub fn validate_input(&self, nsid: &str, input: &Value) -> Checked {
        let def = self.def_of(nsid, &["procedure"], "input")?;
        let schema = &def["input"]["schema"];
        if schema.is_null() {
            return Ok(());
        }
        special(input, "input")?;
        self.one_of(nsid, schema, input, "input", true)
    }

    /// The parameters a query string's `pairs` give the method `nsid`,
    /// each read as its def's type: an integer or a boolean where one
    /// parses, all of a name's values for an array. What does not parse
    /// is left a string, for [`Lexicons::validate_params`] to refuse; a
    /// name the method does not take is left out.
    pub fn query_params(&self, nsid: &str, pairs: &[(String, String)]) -> Map<String, Value> {
        let properties = self.def(nsid).map(|d| &d["parameters"]["properties"]);
        let mut out = Map::new();
        let Some(properties) = properties.and_then(Value::as_object) else {
            return out;
        };
        for (key, prop) in properties {
            let given = pairs.iter().filter(|(k, _)| k == key);
            let values: Vec<Value> = given.map(|(_, v)| typed(&prop["items"], prop, v)).collect();
            if prop["type"] == "array" {
                out.insert(key.clone(), Value::Array(values));
            } else if let Some(first) = values.into_iter().next() {
                out.insert(key.clone(), first);
            }
        }
        out.retain(|_, v| v.as_array().is_none_or(|a| !a.is_empty()));
        out
    }

    /// Checks `value` against `def`, written in the document `nsid`: a ref
    /// or a union followed to the def it names, which then cannot be one.
    /// `object`, for a method's input, takes it as an object whatever the
    /// def it names.
    fn one_of(&self, nsid: &str, def: &Value, value: &Value, path: &str, object: bool) -> Checked {
        let (nsid, concrete) = match def["type"].as_str() {
            Some("union") => {
                let Some(kind) = value.get("$type").and_then(Value::as_str) else {
                    return fail(
                        path,
                        "must be an object which includes the \"$type\" property",
                    );
                };
                let refs: Vec<String> = def["refs"]
                    .as_array()
                    .into_iter()
                    .flatten()
                    .filter_map(Value::as_str)
                    .map(|r| absolute(nsid, r))
                    .collect();
                let listed = !kind.starts_with('#') && refs.contains(&absolute("", kind));
                if !listed && (def["closed"] == true || kind.starts_with('#')) {
                    return fail(path, format!("$type must be one of {}", refs.join(", ")));
                }
                if !listed {
                    return Ok(());
                }
                self.named(nsid, kind, path)?
            }
            Some("ref") => self.named(nsid, def["ref"].as_str().unwrap_or_default(), path)?,
            _ => (nsid.to_string(), def),
        };
        if object {
            self.object(&nsid, concrete, value, path)
        } else {
            self.field(&nsid, concrete, value, path)
        }
    }

    /// The def `reference`, written in `nsid`, names.
    fn named(
        &self,
        nsid: &str,
        reference: &str,
        path: &str,
    ) -> Result<(String, &Value), LexiconError> {
        self.resolve(nsid, reference).ok_or_else(|| LexiconError {
            path: path.to_string(),
            message: format!("no lexicon {}", absolute(nsid, reference)),
        })
    }

    /// Checks `value` against the field type `def`, written in `nsid`.
    fn field(&self, nsid: &str, def: &Value, value: &Value, path: &str) -> Checked {
        match def["type"].as_str().unwrap_or_default() {
            "object" => self.object(nsid, def, value, path),
            "array" => self.array(nsid, def, value, path),
            "blob" => blob(def, value, path),
            "boolean" => boolean(def, value, path),
            "integer" => integer(def, value, path),
            "string" => string(def, value, path),
            "bytes" => bytes(def, value, path),
            "cid-link" => match link(value) {
                Some(Ok(_)) => Ok(()),
                _ => fail(path, "must be a CID"),
            },
            "unknown" if value.is_object() || value.is_array() => Ok(()),
            "unknown" => fail(path, "must be an object"),
            other => fail(
                path,
                format!("is checked against a {other}, which is no field's type"),
            ),
        }
    }

    fn object(&self, nsid: &str, def: &Value, value: &Value, path: &str) -> Checked {
        let Some(map) = value.as_object() else {
            return fail(path, "must be an object");
        };
        let Some(properties) = def["properties"].as_object() else {
            return Ok(());
        };
        for (key, prop) in properties {
            let at = format!("{path}.{key}");
            match map.get(key) {
                Some(Value::Null) if listed(def, "nullable", key) => {}
                Some(v) => self.one_of(nsid, prop, v, &at, false)?,
                None if listed(def, "required", key) && !defaulted(prop) => {
                    fail(path, format!("must have the property \"{key}\""))?;
                }
                None => {}
            }
        }
        Ok(())
    }

    fn array(&self, nsid: &str, def: &Value, value: &Value, path: &str) -> Checked {
        let Some(items) = value.as_array() else {
            return fail(path, "must be an array");
        };
        if let Some(max) = def["maxLength"]
            .as_u64()
            .filter(|m| items.len() as u64 > *m)
        {
            fail(path, format!("must not have more than {max} elements"))?;
        }
        if let Some(min) = def["minLength"]
            .as_u64()
            .filter(|m| (items.len() as u64) < *m)
        {
            fail(path, format!("must not have fewer than {min} elements"))?;
        }
        for (i, item) in items.iter().enumerate() {
            self.one_of(nsid, &def["items"], item, &format!("{path}[{i}]"), false)?;
        }
        Ok(())
    }
}

/// The NSID a reference is in.
fn nsid_of(reference: &str) -> &str {
    reference.split('#').next().unwrap_or_default()
}

/// `reference`, written in `nsid`, as `nsid#name`.
fn absolute(nsid: &str, reference: &str) -> String {
    match reference.split_once('#') {
        Some(("", name)) => format!("{nsid}#{name}"),
        Some(_) => reference.to_string(),
        None => format!("{reference}#main"),
    }
}

/// Whether `def`'s list `list` (`required`, `nullable`) holds `key`.
fn listed(def: &Value, list: &str, key: &str) -> bool {
    def[list]
        .as_array()
        .is_some_and(|l| l.iter().any(|k| k == key))
}

/// Whether a field left out takes its def's default instead.
fn defaulted(def: &Value) -> bool {
    match def["type"].as_str() {
        Some("string") => def["default"].is_string(),
        Some("integer") => def["default"].is_number(),
        Some("boolean") => def["default"].is_boolean(),
        _ => false,
    }
}

/// A query string's `value` as the type `def` (or, for an array, `items`)
/// names.
fn typed(items: &Value, def: &Value, value: &str) -> Value {
    let kind = if def["type"] == "array" {
        &items["type"]
    } else {
        &def["type"]
    };
    let parsed = match kind.as_str() {
        Some("integer") => value.parse::<i64>().ok().map(Value::from),
        Some("boolean") => value.parse::<bool>().ok().map(Value::from),
        _ => None,
    };
    parsed.unwrap_or_else(|| Value::from(value))
}

/// Refuses a link or bytes in `value` that is not one, wherever it is,
/// as reading JSON as atproto data does: `{"$link": cid}` holding no CID,
/// `{"$bytes": base64}` holding no base64.
fn special(value: &Value, path: &str) -> Checked {
    match value {
        Value::Array(items) => items
            .iter()
            .enumerate()
            .try_for_each(|(i, v)| special(v, &format!("{path}[{i}]"))),
        Value::Object(map) if map.len() == 1 && map.contains_key("$link") => match link(value) {
            Some(Ok(_)) => Ok(()),
            _ => fail(path, "must hold a CID in $link"),
        },
        Value::Object(map) if map.len() == 1 && map.contains_key("$bytes") => {
            match decoded(value) {
                Some(_) => Ok(()),
                None => fail(path, "must hold base64 in $bytes"),
            }
        }
        Value::Object(map) => map
            .iter()
            .try_for_each(|(k, v)| special(v, &format!("{path}.{k}"))),
        _ => Ok(()),
    }
}

/// The bytes `{"$bytes": base64}` holds, or `None` for what is not that.
fn decoded(value: &Value) -> Option<Vec<u8>> {
    let map = value.as_object().filter(|m| m.len() == 1)?;
    BASE64.decode(map.get("$bytes")?.as_str()?).ok()
}

/// What `{"$link": cid}` holds: `None` for what is not that, an error for
/// one holding no CID.
fn link(value: &Value) -> Option<Result<(), ()>> {
    let map = value.as_object().filter(|m| m.len() == 1)?;
    let cid = map.get("$link")?;
    Some(match cid.as_str() {
        Some(cid) if cid.len() <= 2048 && is_cid(cid) => Ok(()),
        _ => Err(()),
    })
}

/// Whether `text` is a CID, as the reference parses one: version 1 in
/// base32 (`b…`) or base58 (`z…`), or version 0 (`Qm…`).
fn is_cid(text: &str) -> bool {
    let bytes = match text.as_bytes().first() {
        Some(b'b') => base32::decode(base32::LOWER, &text[1..]),
        Some(b'z') => bs58::decode(&text[1..]).into_vec().ok(),
        Some(b'Q') => {
            let v0 = bs58::decode(text).into_vec().ok();
            return v0.is_some_and(|b| b.len() == 34 && b[..2] == [0x12, 0x20]);
        }
        _ => None,
    };
    bytes.is_some_and(|b| is_cid_v1(&b))
}

/// Whether `bytes` are a binary CID of version 1: the version, a codec,
/// and a multihash holding exactly the digest it says.
fn is_cid_v1(bytes: &[u8]) -> bool {
    let mut rest = bytes;
    let mut next = || {
        let (n, read) = varint(rest)?;
        rest = &rest[read..];
        Some(n)
    };
    let (Some(1), Some(_codec), Some(_hash), Some(len)) = (next(), next(), next(), next()) else {
        return false;
    };
    rest.len() as u64 == len
}

/// An unsigned varint at the start of `bytes`, and how many bytes it took.
fn varint(bytes: &[u8]) -> Option<(u64, usize)> {
    let mut n = 0u64;
    for (i, b) in bytes.iter().take(9).enumerate() {
        n |= u64::from(b & 0x7f) << (7 * i);
        if b & 0x80 == 0 {
            return Some((n, i + 1));
        }
    }
    None
}

fn boolean(def: &Value, value: &Value, path: &str) -> Checked {
    let Some(b) = value.as_bool() else {
        return fail(path, "must be a boolean");
    };
    match def["const"].as_bool() {
        Some(c) if c != b => fail(path, format!("must be {c}")),
        _ => Ok(()),
    }
}

/// `value` as an integer, as JSON numbers are one: `2.0` is.
fn as_integer(value: &Value) -> Option<f64> {
    let n = value.as_f64()?;
    (n.fract() == 0.0).then_some(n)
}

fn integer(def: &Value, value: &Value, path: &str) -> Checked {
    let Some(n) = as_integer(value) else {
        return fail(path, "must be an integer");
    };
    if let Some(c) = def["const"].as_f64().filter(|c| *c != n) {
        fail(path, format!("must be {c}"))?;
    }
    if let Some(e) = def["enum"].as_array()
        && !e.iter().any(|x| x.as_f64() == Some(n))
    {
        fail(path, format!("must be one of {}", Value::from(e.clone())))?;
    }
    if let Some(max) = def["maximum"].as_f64().filter(|m| n > *m) {
        fail(path, format!("can not be greater than {max}"))?;
    }
    if let Some(min) = def["minimum"].as_f64().filter(|m| n < *m) {
        fail(path, format!("can not be less than {min}"))?;
    }
    Ok(())
}

fn string(def: &Value, value: &Value, path: &str) -> Checked {
    let Some(s) = value.as_str() else {
        return fail(path, "must be a string");
    };
    if let Some(c) = def["const"].as_str().filter(|c| c != &s) {
        fail(path, format!("must be {c}"))?;
    }
    if let Some(e) = def["enum"].as_array()
        && !e.iter().any(|x| x == s)
    {
        fail(path, format!("must be one of {}", Value::from(e.clone())))?;
    }
    let bytes = s.len() as u64;
    if let Some(max) = def["maxLength"].as_u64().filter(|m| bytes > *m) {
        fail(path, format!("must not be longer than {max} bytes"))?;
    }
    if let Some(min) = def["minLength"].as_u64().filter(|m| bytes < *m) {
        fail(path, format!("must not be shorter than {min} bytes"))?;
    }
    if def["maxGraphemes"].is_u64() || def["minGraphemes"].is_u64() {
        let graphemes = s.graphemes(true).count() as u64;
        if let Some(max) = def["maxGraphemes"].as_u64().filter(|m| graphemes > *m) {
            fail(path, format!("must not be longer than {max} graphemes"))?;
        }
        if let Some(min) = def["minGraphemes"].as_u64().filter(|m| graphemes < *m) {
            fail(path, format!("must not be shorter than {min} graphemes"))?;
        }
    }
    match def["format"].as_str() {
        Some(format) if !is_format(format, s) => fail(path, format!("must be a valid {format}")),
        _ => Ok(()),
    }
}

fn bytes(def: &Value, value: &Value, path: &str) -> Checked {
    let Some(b) = decoded(value) else {
        return fail(path, "must be a byte array");
    };
    let len = b.len() as u64;
    if let Some(max) = def["maxLength"].as_u64().filter(|m| len > *m) {
        fail(path, format!("must not be larger than {max} bytes"))?;
    }
    if let Some(min) = def["minLength"].as_u64().filter(|m| len < *m) {
        fail(path, format!("must not be smaller than {min} bytes"))?;
    }
    Ok(())
}

/// A blob's type and size, when `value` is one: `{$type: "blob", ref,
/// mimeType, size}`, or the older `{cid, mimeType}`, whose size is unknown.
fn blob_ref(value: &Value) -> Option<(&str, Option<f64>)> {
    let map = value.as_object()?;
    let mime = map.get("mimeType")?.as_str()?;
    let keys = |want: &[&str]| map.len() == want.len() && want.iter().all(|k| map.contains_key(*k));
    if keys(&["$type", "ref", "mimeType", "size"]) {
        let typed = map["$type"] == "blob" && link(&map["ref"]) == Some(Ok(()));
        let size = map["size"].as_f64().filter(|_| typed)?;
        return Some((mime, Some(size)));
    }
    let cid = map.get("cid")?.as_str()?;
    (keys(&["cid", "mimeType"]) && is_cid(cid)).then_some((mime, None))
}

fn blob(def: &Value, value: &Value, path: &str) -> Checked {
    let Some((mime, size)) = blob_ref(value) else {
        return fail(path, "should be a blob ref");
    };
    if let Some(max) = def["maxSize"].as_f64()
        && size.is_some_and(|s| s > max)
    {
        fail(path, format!("must not be larger than {max} bytes"))?;
    }
    let accept: Vec<&str> = def["accept"]
        .as_array()
        .into_iter()
        .flatten()
        .filter_map(Value::as_str)
        .collect();
    let accepted = |pattern: &&str| match pattern.strip_suffix("/*") {
        Some(kind) => kind == "*" || mime.split('/').next() == Some(kind),
        None => *pattern == mime,
    };
    if !accept.is_empty() && !accept.iter().any(accepted) {
        fail(
            path,
            format!("must be one of {} types, not {mime}", accept.join(", ")),
        )?;
    }
    Ok(())
}

/// Whether `s` is well formed as the string format `format`; a format
/// this does not know takes any string.
pub fn is_format(format: &str, s: &str) -> bool {
    match format {
        "datetime" => is_datetime(s),
        "uri" => is_uri(s),
        "at-uri" => is_at_uri(s),
        "did" => is_did(s),
        "handle" => is_handle(s),
        "at-identifier" => is_at_identifier(s),
        "nsid" => is_nsid(s),
        "cid" => is_cid_string(s),
        "language" => is_language(s),
        "tid" => is_tid(s),
        "record-key" => is_record_key(s),
        _ => true,
    }
}

/// A DID, or a handle.
pub fn is_at_identifier(s: &str) -> bool {
    if s.starts_with("did:") {
        is_did(s)
    } else {
        is_handle(s)
    }
}

/// A CID as the `cid` format writes one: 8 to 256 letters, digits, `+`
/// and `=`, and never a version 0 CID's `Qmb`.
pub fn is_cid_string(s: &str) -> bool {
    (8..=256).contains(&s.len())
        && s.bytes()
            .all(|c| c.is_ascii_alphanumeric() || c == b'+' || c == b'=')
        && !s.starts_with("Qmb")
}

/// A URI: a scheme (a letter, then letters, digits, `+`, `-` and `.`), a
/// colon, and no whitespace, 8 KiB at most.
pub fn is_uri(s: &str) -> bool {
    let Some((scheme, rest)) = s.split_once(':') else {
        return false;
    };
    let rest = rest.strip_prefix("//").unwrap_or(rest);
    s.len() <= 8192
        && scheme.starts_with(|c: char| c.is_ascii_alphabetic())
        && scheme
            .bytes()
            .all(|c| c.is_ascii_alphanumeric() || b"+-.".contains(&c))
        && !rest.starts_with('/')
        && !rest.is_empty()
        && !rest.chars().any(char::is_whitespace)
}

/// An AT URI: an authority, and a collection and a record key; or a
/// permissioned space, `at://<did>/space/<type>/<skey>`, and a record in
/// one, `…/<writer>/<collection>/<rkey>`. A fragment is a JSON pointer.
pub fn is_at_uri(s: &str) -> bool {
    let allowed = |c: u8| c.is_ascii_alphanumeric() || b"._~:@!$&'()*+,;=%/\\[]#?-".contains(&c);
    let Some(rest) = s.strip_prefix("at://") else {
        return false;
    };
    if s.len() > 8192 || !s.bytes().all(allowed) || rest.contains('?') {
        return false;
    }
    let (path, fragment) = match rest.split_once('#') {
        Some((path, fragment)) => (path, Some(fragment)),
        None => (rest, None),
    };
    let parts: Vec<&str> = path.split('/').collect();
    let well_formed = match parts[..] {
        [authority, "space", kind, skey] => is_space(authority, kind, skey),
        [authority, "space", kind, skey, writer, collection, rkey] => {
            is_space(authority, kind, skey)
                && is_did(writer)
                && is_nsid(collection)
                && is_record_key(rkey)
        }
        [authority] => is_at_identifier(authority),
        [authority, collection] => is_at_identifier(authority) && is_nsid(collection),
        [authority, collection, rkey] => {
            is_at_identifier(authority) && is_nsid(collection) && is_record_key(rkey)
        }
        _ => false,
    };
    well_formed && fragment.is_none_or(is_pointer)
}

fn is_space(authority: &str, kind: &str, skey: &str) -> bool {
    is_did(authority) && is_nsid(kind) && is_record_key(skey)
}

/// A JSON pointer as an AT URI's fragment writes one: `/…`, of URI
/// characters, percent-encoding UTF-8.
fn is_pointer(s: &str) -> bool {
    let allowed = |c: u8| c.is_ascii_alphanumeric() || b"._~:@!$&')(*+,;=%[]/-".contains(&c);
    if !s.starts_with('/') || !s.bytes().all(allowed) {
        return false;
    }
    let mut out = Vec::new();
    let mut bytes = s.bytes();
    while let Some(c) = bytes.next() {
        if c != b'%' {
            out.push(c);
            continue;
        }
        let hex: String = bytes.by_ref().take(2).map(char::from).collect();
        match u8::from_str_radix(&hex, 16) {
            Ok(b) if hex.len() == 2 => out.push(b),
            _ => return false,
        }
    }
    String::from_utf8(out).is_ok()
}

/// The digits `s` is, all of them.
fn digits(s: &str) -> Option<i64> {
    s.bytes()
        .all(|c| c.is_ascii_digit())
        .then(|| s.parse().ok())?
}

/// A datetime as the spec writes one: `YYYY-MM-DDTHH:MM:SS`, a fraction
/// of a second if any, and `Z` or `±HH:MM` (not `-00:00`); 64 characters
/// at most, and in UTC within the years 0 to 9999. A day past its month's
/// end runs on into the next month, as the reference reads one.
pub fn is_datetime(s: &str) -> bool {
    let b = s.as_bytes();
    if s.len() > 64 || s.len() < 20 || s.ends_with("-00:00") || !s.is_ascii() {
        return false;
    }
    let shape = b[4] == b'-' && b[7] == b'-' && b[10] == b'T' && b[13] == b':' && b[16] == b':';
    let number = |from: usize, to: usize| digits(&s[from..to]);
    let (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second)) = (
        number(0, 4),
        number(5, 7),
        number(8, 10),
        number(11, 13),
        number(14, 16),
        number(17, 19),
    ) else {
        return false;
    };
    let rest = &s[19..];
    let zone = match rest.find(['Z', '+', '-']) {
        Some(at) => &rest[at..],
        None => return false,
    };
    let fraction = &rest[..rest.len() - zone.len()];
    let fraction_ok = fraction.is_empty()
        || fraction.len() > 1
            && fraction.starts_with('.')
            && fraction[1..].bytes().all(|c| c.is_ascii_digit());
    let offset = match zone.as_bytes() {
        [b'Z'] => Some(0),
        [sign @ (b'+' | b'-'), h1, h2, b':', m1, m2] => {
            let hm = |a: &u8, b: &u8| digits(&format!("{}{}", *a as char, *b as char));
            match (hm(h1, h2), hm(m1, m2)) {
                (Some(h), Some(m)) if h < 24 && m < 60 => {
                    Some(if *sign == b'-' { -1 } else { 1 } * (h * 60 + m))
                }
                _ => None,
            }
        }
        _ => None,
    };
    let in_range = (1..=12).contains(&month)
        && (1..=31).contains(&day)
        && hour < 24
        && minute < 60
        && second < 60;
    let Some(offset) = offset.filter(|_| shape && fraction_ok && in_range) else {
        return false;
    };
    let minutes = (days_from_civil(year, month) + day - 1) * 1440 + hour * 60 + minute - offset;
    let first = days_from_civil(0, 1) * 1440;
    let last = days_from_civil(10000, 1) * 1440;
    (first..last).contains(&minutes)
}

/// The days from 1970-01-01 to the first of `month` in `year`.
fn days_from_civil(year: i64, month: i64) -> i64 {
    let y = if month <= 2 { year - 1 } else { year };
    let era = y.div_euclid(400);
    let yoe = y - era * 400;
    let mp = (month + 9) % 12;
    let doy = (153 * mp + 2) / 5;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146097 + doe - 719468
}

/// The tags RFC 5646 keeps from before its grammar.
const GRANDFATHERED: [&str; 26] = [
    "en-GB-oed",
    "i-ami",
    "i-bnn",
    "i-default",
    "i-enochian",
    "i-hak",
    "i-klingon",
    "i-lux",
    "i-mingo",
    "i-navajo",
    "i-pwn",
    "i-tao",
    "i-tay",
    "i-tsu",
    "sgn-BE-FR",
    "sgn-BE-NL",
    "sgn-CH-DE",
    "art-lojban",
    "cel-gaulish",
    "no-bok",
    "no-nyn",
    "zh-guoyu",
    "zh-hakka",
    "zh-min",
    "zh-min-nan",
    "zh-xiang",
];

/// A BCP 47 language tag (RFC 5646), its primary subtag lower case, no
/// variant or extension repeated.
pub fn is_language(s: &str) -> bool {
    if GRANDFATHERED.contains(&s) {
        return true;
    }
    let tags: Vec<&str> = s.split('-').collect();
    let alpha = |t: &str, n: std::ops::RangeInclusive<usize>| {
        n.contains(&t.len()) && t.bytes().all(|c| c.is_ascii_alphabetic())
    };
    let alnum = |t: &str, n: std::ops::RangeInclusive<usize>| {
        n.contains(&t.len()) && t.bytes().all(|c| c.is_ascii_alphanumeric())
    };
    let private = |rest: &[&str]| !rest.is_empty() && rest.iter().all(|t| alnum(t, 1..=8));
    if tags[0].eq_ignore_ascii_case("x") {
        return private(&tags[1..]);
    }
    let primary = tags[0];
    if !(2..=3).contains(&primary.len()) || !primary.bytes().all(|c| c.is_ascii_lowercase()) {
        return false;
    }
    let mut i = 1;
    while i < tags.len() && i <= 3 && alpha(tags[i], 3..=3) {
        i += 1;
    }
    if i < tags.len() && alpha(tags[i], 4..=4) {
        i += 1;
    }
    let region = |t: &str| alpha(t, 2..=2) || t.len() == 3 && t.bytes().all(|c| c.is_ascii_digit());
    if i < tags.len() && region(tags[i]) {
        i += 1;
    }
    let variant = |t: &str| alnum(t, 5..=8) || alnum(t, 4..=4) && t.as_bytes()[0].is_ascii_digit();
    let mut seen = Vec::new();
    while i < tags.len() && variant(tags[i]) {
        seen.push(tags[i].to_ascii_lowercase());
        i += 1;
    }
    let singleton = |t: &str| alnum(t, 1..=1) && !t.eq_ignore_ascii_case("x");
    while i < tags.len() && singleton(tags[i]) {
        seen.push(tags[i].to_ascii_lowercase());
        let start = i + 1;
        i = start;
        while i < tags.len() && alnum(tags[i], 2..=8) {
            i += 1;
        }
        if i == start {
            return false;
        }
    }
    let mut unique = seen.clone();
    unique.sort();
    unique.dedup();
    let ended = i == tags.len() || tags[i].eq_ignore_ascii_case("x") && private(&tags[i + 1..]);
    ended && unique.len() == seen.len()
}