// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Why this library would not do what it was asked.
///
/// The variants are the places a file can stop being a file: the header does not
/// say what the format says it says, a stanza of a known type does not either,
/// the MAC over the header does not check out, no identity opened any stanza, a
/// payload chunk did not authenticate, the text was not the armor it claimed to
/// be, a key is not the size this format defines, the random source the caller
/// handed in could not give what the file needs, or an input was longer than
/// this library will read.
///
/// `at` is an offset in bytes from the start of the input the call was given.
pub(all) suberror Refused {
Header(at~ : Int32, want~ : String)
Stanza(want~ : String)
Mac
NoMatch
Payload(at~ : Int32)
Armor(at~ : Int32, want~ : String)
Key(want~ : Int32, got~ : Int32)
Random(@moonrand.Unavailable)
Limit(want~ : Int32, got~ : Int32)
} derive(Eq, Debug)
///|
pub extend Refused with Eq::{equal, not_equal}
///|
pub extend Refused with Show::{to_string, output}
///|
/// What went wrong, in one line, with the offset where there is one. Printing a
/// refusal is what a caller does when it has nobody to hand it to.
pub impl Show for Refused with fn output(self, logger) {
match self {
Header(at~, want~) =>
logger.write_string("not a header at byte \{at}: \{want}")
Stanza(want~) =>
logger.write_string("a stanza is not as specified: \{want}")
Mac => logger.write_string("the header MAC does not verify")
NoMatch => logger.write_string("no identity opens this file")
Payload(at~) => logger.write_string("the payload fails at byte \{at}")
Armor(at~, want~) => logger.write_string("not armor at byte \{at}: \{want}")
Key(want~, got~) =>
logger.write_string("a file key of \{want} bytes, not \{got}")
Random(why) => logger.write_string("no randomness: \{why}")
Limit(want~, got~) =>
logger.write_string("at most \{want} bytes, \{got} given")
}
}
///|
pub extend Refused with Debug::{to_repr}
///|
/// One recipient stanza: the type, the arguments after it, and the body that
/// carries the file key wrapped for whoever holds the matching secret.
///
/// A recipient type is nothing more than a name, a list of arguments it defines
/// the meaning of, and a body; the header carries them without understanding
/// them, which is what lets a type this library has never heard of travel
/// through it.
pub(all) struct Stanza {
kind : String
args : Array[String]
body : Bytes
}
///|
pub fn Stanza::new(
kind~ : String,
args~ : Array[String],
body~ : Bytes,
) -> Self {
{ kind, args, body, }
}
///|
/// `len` bytes from `random`, or the reason it had none as a refusal.
///
/// A recipient type calls this for its ephemeral material, so that a failing
/// source arrives as the one error type every call here raises.
pub fn draw(random : &@moonrand.Source, len : Int32) -> Bytes raise Refused {
@moonrand.bytes(len, random) catch {
why => raise Refused::Random(why)
}
}
///|
/// A recipient type, from the encrypting side: it takes the file key and wraps
/// it into a stanza only the matching identity can unwrap.
///
/// `random` is where the ephemeral material comes from. It is passed rather
/// than found so that the one source a caller chose serves every stanza of a
/// file, and so that a `Replay` seals the same file twice.
pub(open) trait Recipient {
fn wrap(Self, file_key~ : BytesView, random~ : &@moonrand.Source) -> Stanza raise Refused
}
///|
/// A recipient type, from the decrypting side: given a stanza, either the file
/// key or `None` for "not mine, try the next one".
///
/// A stanza of a type this identity does not implement is `None`, and so is one
/// that is well formed but sealed to another key. A stanza that claims to be
/// this type and is malformed is a refusal, because at that point the file is
/// broken rather than addressed to somebody else.
pub(open) trait Identity {
fn unwrap(Self, stanza~ : Stanza) -> Bytes? raise Refused
}