use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use ferrocrypt_test_support::wire_manifest::{
FCA_ARCHIVE_TLV_DOMAIN, FCA_ENTRY_TLV_DOMAIN, FCA_VERSION_DOMAIN, KEY_TYPE_DOMAIN,
OUTER_TLV_DOMAIN, OUTER_VERSION_DOMAIN, PRIVATE_KEY_TLV_DOMAIN, PRIVATE_KEY_VERSION_DOMAIN,
PUBLIC_KEY_VERSION_DOMAIN, RECIPIENT_TYPE_DOMAIN, capability_parts, field, subject_version,
};
use zeroize::Zeroizing;
use super::{
Breaks, CheckList, Direction, Evidence, LoadedCase, ProfileCaps, Reach, ReaderCapabilities,
Row, Span, corpus_root, limit_profiles, open_pairs, over_kdf_cap, rejected_cases, reopens,
stray_losses, type_name_grammar_breaks,
};
use crate::crypto::kdf::{KDF_PARAMS_SIZE, KdfLimit, KdfParams};
use crate::crypto::keys::{FILE_KEY_SIZE, FileKey, derive_subkeys};
use crate::crypto::stream::STREAM_NONCE_SIZE;
use crate::format::{
BODY_LEN_MAX, EXT_LEN_MAX, FCR_FILE_V1_VERSION, HEADER_FIXED_EXT_LEN_OFFSET,
HEADER_FIXED_FLAGS_OFFSET, HEADER_FIXED_RECIPIENT_COUNT_OFFSET,
HEADER_FIXED_RECIPIENT_ENTRIES_LEN_OFFSET, HEADER_FIXED_SIZE, HEADER_FIXED_STREAM_NONCE_OFFSET,
HEADER_LEN_MAX, HEADER_MAC_SIZE, KIND_ENCRYPTED, MAGIC, PREFIX_FLAGS_OFFSET,
PREFIX_HEADER_LEN_OFFSET, PREFIX_KIND_OFFSET, PREFIX_SIZE, PREFIX_VERSION_OFFSET,
RECIPIENT_COUNT_MAX, read_u16_be, read_u32_be, verify_header_mac,
};
use crate::key::files::read_private_key_bytes;
use crate::key::private::PrivateKeyHeader;
use crate::passphrase::Passphrase;
use crate::recipient::entry::{
ENTRY_BODY_LEN_OFFSET, ENTRY_HEADER_SIZE, ENTRY_RECIPIENT_FLAGS_OFFSET,
ENTRY_TYPE_NAME_LEN_OFFSET, RECIPIENT_FLAG_CRITICAL, RecipientEntry,
};
use crate::recipient::name::TYPE_NAME_MAX_LEN;
use crate::recipient::native::{argon2id, x25519};
use crate::recipient::policy::{NativeRecipientType, enforce_recipient_mixing_policy};
use crate::wire_vector_gen::{CorpusCredential, WALK_ORDER_CASE_PREFIX, read_credential};
use crate::{CryptoError, FormatDefect};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum Fcr {
PrefixShort,
Magic,
Kind,
VersionZero,
VersionUnsupported,
PrefixFlags,
HeaderLenMax,
HeaderLenCap,
HeaderTruncated,
HeaderFixedShort,
HeaderFlags,
RecipientCountRange,
ExtLenMax,
LengthsSum,
RecipientCountCap,
EntryHeaderShort,
TypeNameLen,
BodyLenMax,
FlagsReserved,
EntryFit,
BodyLenCap,
TypeNameGrammar,
TrailingBytes,
UnknownCritical,
NativeFlags,
NativeLength,
Argon2idContent,
X25519Content,
Mixing,
HeaderMacWorkCap,
NoSupportedRecipient,
KdfCap,
KeyUnlock,
KeyAgreement,
PassphraseUnwrap,
KeyUnwrap,
HeaderMac,
}
use Fcr::*;
const TRYING: [Fcr; 6] = [
KdfCap,
KeyUnlock,
KeyAgreement,
PassphraseUnwrap,
KeyUnwrap,
HeaderMac,
];
const PER_ENTRY_STEPS: [Fcr; 5] = [
UnknownCritical,
NativeFlags,
NativeLength,
Argon2idContent,
X25519Content,
];
const FRAMING: [Fcr; 7] = [
EntryHeaderShort,
TypeNameLen,
BodyLenMax,
FlagsReserved,
EntryFit,
BodyLenCap,
TypeNameGrammar,
];
const FRAMING_GROUP: usize = 0;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Pass {
Framing,
UnknownCritical,
NativeFraming,
Content(NativeRecipientType),
}
fn pass(check: Fcr) -> Option<Pass> {
match check {
_ if FRAMING.contains(&check) => Some(Pass::Framing),
UnknownCritical => Some(Pass::UnknownCritical),
NativeFlags | NativeLength => Some(Pass::NativeFraming),
Argon2idContent => Some(Pass::Content(NativeRecipientType::Argon2id)),
X25519Content => Some(Pass::Content(NativeRecipientType::X25519)),
_ => None,
}
}
const NATIVE_TYPES: [NativeRecipientType; 2] = {
match NativeRecipientType::Argon2id {
NativeRecipientType::Argon2id | NativeRecipientType::X25519 => {}
}
[NativeRecipientType::Argon2id, NativeRecipientType::X25519]
};
fn content_check(native: NativeRecipientType) -> Fcr {
match native {
NativeRecipientType::Argon2id => Argon2idContent,
NativeRecipientType::X25519 => X25519Content,
}
}
fn one_entry_breaks_both(a: Fcr, b: Fcr) -> bool {
let pair = |x: Fcr, y: Fcr| (a == x && b == y) || (a == y && b == x);
let across = |xs: &[Fcr], ys: &[Fcr]| xs.iter().any(|&x| ys.iter().any(|&y| pair(x, y)));
let native = [NativeFlags, NativeLength, Argon2idContent, X25519Content];
let content = [Argon2idContent, X25519Content];
!(across(&[TypeNameLen, TypeNameGrammar, UnknownCritical], &native)
|| across(&[BodyLenMax, NativeLength], &content)
|| pair(Argon2idContent, X25519Content))
}
fn exclusive(a: Fcr, b: Fcr) -> bool {
let pair = |x: Fcr, y: Fcr| (a == x && b == y) || (a == y && b == x);
let either = |one: Fcr| a == one || b == one;
let other = |one: Fcr| if a == one { b } else { a };
if pair(VersionZero, VersionUnsupported) || pair(HeaderLenMax, HeaderFixedShort) {
return true;
}
if either(PrefixShort) {
return ![Magic, Kind, VersionZero, VersionUnsupported, PrefixFlags]
.contains(&other(PrefixShort));
}
if either(NoSupportedRecipient) {
return [
NativeFlags,
NativeLength,
Argon2idContent,
X25519Content,
Mixing,
HeaderMacWorkCap,
]
.contains(&other(NoSupportedRecipient));
}
false
}
struct FcrList;
impl CheckList for FcrList {
type Check = Fcr;
fn order() -> &'static [Fcr] {
&[
PrefixShort,
Magic,
Kind,
VersionZero,
VersionUnsupported,
PrefixFlags,
HeaderLenMax,
HeaderLenCap,
HeaderTruncated,
HeaderFixedShort,
HeaderFlags,
RecipientCountRange,
ExtLenMax,
LengthsSum,
RecipientCountCap,
EntryHeaderShort,
TypeNameLen,
BodyLenMax,
FlagsReserved,
EntryFit,
BodyLenCap,
TypeNameGrammar,
TrailingBytes,
UnknownCritical,
NativeFlags,
NativeLength,
Argon2idContent,
X25519Content,
Mixing,
HeaderMacWorkCap,
NoSupportedRecipient,
KdfCap,
KeyUnlock,
KeyAgreement,
PassphraseUnwrap,
KeyUnwrap,
HeaderMac,
]
}
fn class(check: Fcr) -> &'static str {
match check {
PrefixShort | HeaderTruncated => "truncated",
Magic => "bad_magic",
Kind => "wrong_kind",
VersionZero | PrefixFlags | HeaderFixedShort | HeaderFlags | LengthsSum => {
"malformed_header"
}
VersionUnsupported => "unsupported_outer_version",
HeaderLenMax => "oversized_header",
HeaderLenCap | RecipientCountCap | BodyLenCap | HeaderMacWorkCap | KdfCap => {
"resource_cap_exceeded"
}
RecipientCountRange => "recipient_count_out_of_range",
ExtLenMax => "extension_region_too_large",
EntryHeaderShort | TypeNameLen | BodyLenMax | EntryFit | TrailingBytes
| NativeFlags | NativeLength | X25519Content | KeyAgreement => {
"malformed_recipient_entry"
}
FlagsReserved => "recipient_flags_reserved",
TypeNameGrammar => "malformed_type_name",
UnknownCritical => "unknown_critical_recipient",
Argon2idContent => "invalid_kdf_parameters",
Mixing => "incompatible_recipients",
NoSupportedRecipient => "no_supported_recipient",
KeyUnlock => "private_key_unlock_failed",
PassphraseUnwrap | KeyUnwrap => "recipient_unwrap_failed",
HeaderMac => "header_authentication_failed",
}
}
fn claimed(earlier: Fcr, later: Fcr) -> bool {
if exclusive(earlier, later) {
return false;
}
!TRYING.contains(&later) || earlier == HeaderMacWorkCap
}
const DIRECTION_GROUPS: usize = 1 + PER_ENTRY_STEPS.len();
fn direction_group(check: Fcr) -> usize {
PER_ENTRY_STEPS
.iter()
.position(|&step| step == check)
.map_or(FRAMING_GROUP, |index| index + 1)
}
fn directions(group: usize) -> &'static [Direction] {
if group == FRAMING_GROUP {
&[Direction::FrontToBack]
} else {
&Direction::BOTH
}
}
fn specification_report(broken: &[Break]) -> Option<&'static str> {
for stage in Self::order().chunk_by(|a, b| pass(*a).is_some() && pass(*a) == pass(*b)) {
let entries: BTreeSet<Option<usize>> = broken
.iter()
.filter(|(check, _)| stage.contains(check))
.map(|(_, entry)| *entry)
.collect();
for entry in entries {
if let Some(&check) = stage
.iter()
.find(|&&check| broken.contains(&(check, entry)))
{
return Some(Self::class(check));
}
}
}
None
}
}
#[derive(Default)]
struct Capabilities {
outer_versions: BTreeSet<u8>,
recipient_types: BTreeSet<String>,
}
impl ReaderCapabilities for Capabilities {
type Check = Fcr;
fn declaring(run: &[String]) -> (Self, BTreeSet<Fcr>) {
let mut capabilities = Self::default();
let mut changed = BTreeSet::new();
for capability_id in run {
let (domain, subject) = capability_parts(capability_id)
.unwrap_or_else(|| panic!("{capability_id}: not a capability ID"));
match domain {
OUTER_VERSION_DOMAIN => {
let version = subject_version(subject)
.unwrap_or_else(|| panic!("{capability_id}: not a version"));
capabilities.outer_versions.insert(version);
changed.insert(VersionUnsupported);
}
RECIPIENT_TYPE_DOMAIN => {
capabilities.recipient_types.insert(subject.to_string());
changed.extend(
[
UnknownCritical,
NoSupportedRecipient,
Mixing,
HeaderMacWorkCap,
]
.into_iter()
.chain(TRYING),
);
}
FCA_VERSION_DOMAIN
| PUBLIC_KEY_VERSION_DOMAIN
| PRIVATE_KEY_VERSION_DOMAIN
| OUTER_TLV_DOMAIN
| PRIVATE_KEY_TLV_DOMAIN
| FCA_ARCHIVE_TLV_DOMAIN
| FCA_ENTRY_TLV_DOMAIN
| KEY_TYPE_DOMAIN => {}
other => panic!("{capability_id}: unknown capability domain {other}"),
}
}
(capabilities, changed)
}
}
impl Capabilities {
fn supports(&self, type_name: &[u8]) -> bool {
native_type(type_name).is_some()
|| std::str::from_utf8(type_name).is_ok_and(|name| self.recipient_types.contains(name))
}
}
fn native_type(type_name: &[u8]) -> Option<NativeRecipientType> {
std::str::from_utf8(type_name)
.ok()
.and_then(NativeRecipientType::from_type_name)
}
type Break = (Fcr, Option<usize>);
#[derive(Clone, Copy, PartialEq, Eq)]
enum RegionEnd {
Declared,
DeclaredInHeader,
Header,
BeforeExtensions,
DeclaredBeforeExtensions,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EntryEnd {
Region,
Header,
File,
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct Reading {
fixed_in_header: bool,
region_end: RegionEnd,
entry_end: EntryEnd,
}
impl Reading {
const SPECIFICATION: Reading = Reading {
fixed_in_header: true,
region_end: RegionEnd::Declared,
entry_end: EntryEnd::Region,
};
fn all() -> impl Iterator<Item = Reading> {
let region_ends = [
RegionEnd::Declared,
RegionEnd::DeclaredInHeader,
RegionEnd::Header,
RegionEnd::BeforeExtensions,
RegionEnd::DeclaredBeforeExtensions,
];
let entry_ends = [EntryEnd::Region, EntryEnd::Header, EntryEnd::File];
[true, false].into_iter().flat_map(move |fixed_in_header| {
region_ends.into_iter().flat_map(move |region_end| {
entry_ends.into_iter().map(move |entry_end| Reading {
fixed_in_header,
region_end,
entry_end,
})
})
})
}
}
struct Entry {
type_name: Option<Vec<u8>>,
flags: u16,
body_len: u32,
body: Vec<u8>,
}
impl Entry {
fn whole_body(&self) -> Option<&[u8]> {
(self.body.len() == self.body_len as usize).then_some(self.body.as_slice())
}
}
#[derive(Default)]
struct Walk {
entries: Vec<Entry>,
complete: bool,
}
fn structural_breaks(
bytes: &[u8],
profile: &Row,
capabilities: &Capabilities,
reading: Reading,
) -> (Breaks<Fcr>, Walk) {
let mut certain = BTreeSet::new();
let mut uncertain = BTreeSet::new();
let n = bytes.len();
if n < PREFIX_SIZE {
certain.insert((PrefixShort, None));
}
let magic_held = n.min(MAGIC.len());
if bytes[..magic_held] != MAGIC[..magic_held] {
if magic_held == MAGIC.len() {
certain.insert((Magic, None));
} else {
uncertain.insert((Magic, None));
}
}
if bytes
.get(PREFIX_KIND_OFFSET)
.is_some_and(|&kind| kind != KIND_ENCRYPTED)
{
certain.insert((Kind, None));
}
match bytes.get(PREFIX_VERSION_OFFSET).copied() {
Some(0) => {
certain.insert((VersionZero, None));
}
Some(version)
if version != FCR_FILE_V1_VERSION
&& !capabilities.outer_versions.contains(&version) =>
{
certain.insert((VersionUnsupported, None));
}
_ => {}
}
if read_u16_be(bytes, PREFIX_FLAGS_OFFSET).is_ok_and(|flags| flags != 0) {
certain.insert((PrefixFlags, None));
}
let no_entries = |certain, uncertain| {
let breaks = Breaks {
certain,
uncertain,
..Breaks::default()
};
(breaks, Walk::default())
};
let Ok(header_len) = read_u32_be(bytes, PREFIX_HEADER_LEN_OFFSET) else {
return no_entries(certain, uncertain);
};
let header_len = header_len as usize;
if header_len > HEADER_LEN_MAX as usize {
certain.insert((HeaderLenMax, None));
}
let caps = ProfileCaps::new(profile).header();
if header_len > caps.max_header_len as usize {
certain.insert((HeaderLenCap, None));
}
if n < PREFIX_SIZE + header_len + HEADER_MAC_SIZE {
certain.insert((HeaderTruncated, None));
}
if header_len < HEADER_FIXED_SIZE {
certain.insert((HeaderFixedShort, None));
}
let fixed_end = if reading.fixed_in_header {
n.min(PREFIX_SIZE + header_len)
} else {
n
};
let header = &bytes[PREFIX_SIZE..fixed_end];
let flags = read_u16_be(header, HEADER_FIXED_FLAGS_OFFSET).ok();
let count = read_u16_be(header, HEADER_FIXED_RECIPIENT_COUNT_OFFSET).ok();
let entries_len = read_u32_be(header, HEADER_FIXED_RECIPIENT_ENTRIES_LEN_OFFSET).ok();
let ext_len = read_u32_be(header, HEADER_FIXED_EXT_LEN_OFFSET).ok();
if flags.is_some_and(|flags| flags != 0) {
certain.insert((HeaderFlags, None));
}
if count.is_some_and(|count| count == 0 || count > RECIPIENT_COUNT_MAX) {
certain.insert((RecipientCountRange, None));
}
if ext_len.is_some_and(|ext_len| ext_len > EXT_LEN_MAX) {
certain.insert((ExtLenMax, None));
}
if let (Some(entries_len), Some(ext_len)) = (entries_len, ext_len) {
let sum = HEADER_FIXED_SIZE as u64 + u64::from(entries_len) + u64::from(ext_len);
if sum != header_len as u64 {
certain.insert((LengthsSum, None));
}
}
if count.is_some_and(|count| count > caps.max_recipient_count) {
certain.insert((RecipientCountCap, None));
}
let (Some(count), Some(entries_len)) = (count, entries_len) else {
return no_entries(certain, uncertain);
};
let entries_len = entries_len as usize;
let header_room = header_len.saturating_sub(HEADER_FIXED_SIZE);
let before_extensions = ext_len.map(|ext_len| header_room.saturating_sub(ext_len as usize));
let region_len = match reading.region_end {
RegionEnd::Declared => Some(entries_len),
RegionEnd::DeclaredInHeader => Some(entries_len.min(header_room)),
RegionEnd::Header => Some(header_room),
RegionEnd::BeforeExtensions => before_extensions,
RegionEnd::DeclaredBeforeExtensions => before_extensions.map(|end| entries_len.min(end)),
};
let Some(region_len) = region_len else {
return no_entries(certain, uncertain);
};
let region_start = PREFIX_SIZE + HEADER_FIXED_SIZE;
let held_end = match reading.entry_end {
EntryEnd::Region => region_start + region_len,
EntryEnd::Header => PREFIX_SIZE + header_len,
EntryEnd::File => n,
};
let held = bytes.get(region_start..n.min(held_end)).unwrap_or_default();
let walk = walk_region(
held,
region_len,
count,
caps.max_recipient_body_len,
&mut certain,
&mut uncertain,
);
for (index, entry) in walk.entries.iter().enumerate() {
let Some(type_name) = entry.type_name.as_deref() else {
continue;
};
let Some(native) = native_type(type_name) else {
if entry.flags & RECIPIENT_FLAG_CRITICAL != 0 && !capabilities.supports(type_name) {
certain.insert((UnknownCritical, Some(index)));
}
continue;
};
if entry.flags != 0 {
certain.insert((NativeFlags, Some(index)));
}
if entry.body_len as usize != native.body_len() {
certain.insert((NativeLength, Some(index)));
}
if let Some(body) = entry
.whole_body()
.filter(|body| body.len() == native.body_len())
{
if native.validate_body(body).is_err() {
certain.insert((content_check(native), Some(index)));
}
}
}
let list_wide = if walk.complete {
&mut certain
} else {
&mut uncertain
};
let listed: Vec<RecipientEntry> = walk
.entries
.iter()
.map(|entry| RecipientEntry {
type_name: entry
.type_name
.as_deref()
.map(|name| String::from_utf8_lossy(name).into_owned())
.unwrap_or_default(),
recipient_flags: entry.flags,
body: Vec::new(),
})
.collect();
let mixing_refuses = |met: &[usize], counted: bool| {
let entries = met.iter().map(|&index| listed[index].clone()).collect();
mixing_refused(entries, counted.then_some(usize::from(count)))
};
let every_entry: Vec<usize> = (0..listed.len()).collect();
if mixing_refuses(&every_entry, true) {
list_wide.insert((Mixing, None));
}
let supported: Vec<bool> = walk
.entries
.iter()
.map(|entry| {
entry
.type_name
.as_deref()
.is_some_and(|name| capabilities.supports(name))
})
.collect();
let over_work_cap = |met: &[usize]| {
let counted = met.iter().filter(|&&index| supported[index]).count() as u64;
counted * (PREFIX_SIZE + header_len) as u64 > caps.max_header_mac_work_bytes
};
if over_work_cap(&every_entry) {
list_wide.insert((HeaderMacWorkCap, None));
}
if walk.complete && !supported.contains(&true) {
certain.insert((NoSupportedRecipient, None));
}
let mut reach = BTreeMap::new();
let mixing = reach_of(
listed.len(),
|met| mixing_refuses(met, true),
|met| mixing_refuses(met, false),
);
let work_cap = reach_of(listed.len(), over_work_cap, over_work_cap);
for (check, found) in [(Mixing, mixing), (HeaderMacWorkCap, work_cap)] {
if let Some(found) = found {
reach.insert(check, found);
}
}
let breaks = Breaks {
certain,
uncertain,
reach,
first_try: BTreeSet::new(),
};
(breaks, walk)
}
fn mixing_refused(mut met: Vec<RecipientEntry>, declared: Option<usize>) -> bool {
if let Some(declared) = declared {
met.resize(met.len().max(declared), entry_of_type(""));
}
match enforce_recipient_mixing_policy(&met) {
Ok(()) => false,
Err(CryptoError::IncompatibleRecipients { .. }) => true,
Err(other) => panic!("the mixing rules refuse a list for another reason: {other:?}"),
}
}
fn entry_of_type(type_name: &str) -> RecipientEntry {
RecipientEntry {
type_name: type_name.to_string(),
recipient_flags: 0,
body: Vec::new(),
}
}
fn reach_of(
len: usize,
earliest: impl Fn(&[usize]) -> bool,
latest: impl Fn(&[usize]) -> bool,
) -> Option<Reach> {
let span = |direction: Direction| {
let order: Vec<usize> = match direction {
Direction::FrontToBack => (0..len).collect(),
Direction::BackToFront => (0..len).rev().collect(),
};
let found = |broken: &dyn Fn(&[usize]) -> bool| {
let met = (1..=len).collect::<Vec<usize>>();
let first = met.partition_point(|&count| !broken(&order[..count]));
met.get(first).map(|&count| order[count - 1])
};
Some(Span {
first: found(&earliest)?,
last: found(&latest),
})
};
Some(Reach {
front_to_back: span(Direction::FrontToBack)?,
back_to_front: span(Direction::BackToFront)?,
})
}
fn walk_region(
held: &[u8],
region_len: usize,
count: u16,
body_cap: u32,
certain: &mut BTreeSet<Break>,
uncertain: &mut BTreeSet<Break>,
) -> Walk {
let count = usize::from(count);
let mut entries = Vec::new();
let finish = |entries: Vec<Entry>| Walk {
complete: entries.len() == count && entries.iter().all(|entry| entry.type_name.is_some()),
entries,
};
let mut offset = 0;
for index in 0..count {
let room = region_len - offset;
let type_name_len = read_u16_be(held, offset + ENTRY_TYPE_NAME_LEN_OFFSET).ok();
let flags = read_u16_be(held, offset + ENTRY_RECIPIENT_FLAGS_OFFSET).ok();
let body_len = read_u32_be(held, offset + ENTRY_BODY_LEN_OFFSET).ok();
if room < ENTRY_HEADER_SIZE {
certain.insert((EntryHeaderShort, Some(index)));
}
if type_name_len.is_some_and(|len| len == 0 || usize::from(len) > TYPE_NAME_MAX_LEN) {
certain.insert((TypeNameLen, Some(index)));
}
if body_len.is_some_and(|len| len > BODY_LEN_MAX) {
certain.insert((BodyLenMax, Some(index)));
}
if flags.is_some_and(|flags| flags & !RECIPIENT_FLAG_CRITICAL != 0) {
certain.insert((FlagsReserved, Some(index)));
}
let (Some(type_name_len), Some(flags), Some(body_len)) = (type_name_len, flags, body_len)
else {
return finish(entries);
};
let size = ENTRY_HEADER_SIZE as u64 + u64::from(type_name_len) + u64::from(body_len);
let fits = size <= room as u64;
if !fits {
certain.insert((EntryFit, Some(index)));
}
if body_len > body_cap {
certain.insert((BodyLenCap, Some(index)));
}
let name_start = offset + ENTRY_HEADER_SIZE;
let name_end = name_start + usize::from(type_name_len);
let type_name = held.get(name_start..name_end).map(<[u8]>::to_vec);
match type_name.as_deref().and_then(type_name_grammar_breaks) {
Some(true) => {
certain.insert((TypeNameGrammar, Some(index)));
}
Some(false) => {
uncertain.insert((TypeNameGrammar, Some(index)));
}
None => {}
}
let body_end = name_end.saturating_add(body_len as usize);
let body = held
.get(name_end.min(held.len())..body_end.min(held.len()))
.unwrap_or_default()
.to_vec();
entries.push(Entry {
type_name,
flags,
body_len,
body,
});
if !fits {
return finish(entries);
}
offset += size as usize;
}
if offset != region_len {
certain.insert((TrailingBytes, None));
}
finish(entries)
}
type KeptFileKey = Zeroizing<[u8; FILE_KEY_SIZE]>;
struct Credentials {
root: PathBuf,
opened: BTreeMap<(String, usize, u32), Opened>,
tried: BTreeMap<(String, Vec<u8>), Tried>,
}
enum Opened {
None,
Passphrase(Passphrase),
PrivateKey {
kdf_params: KdfParams,
secret: Option<Zeroizing<[u8; x25519::PRIVATE_KEY_SIZE]>>,
},
RefusedKey(String),
}
enum Decided {
Breaks(Breaks<Fcr>),
KeyRefused(String),
}
enum Tried {
Opened(KeptFileKey),
Refused,
ZeroSharedSecret,
}
fn stored_kdf_params(body: &[u8]) -> Option<KdfParams> {
body.get(argon2id::KDF_PARAMS_OFFSET..argon2id::KDF_PARAMS_OFFSET + KDF_PARAMS_SIZE)
.and_then(|field| field.try_into().ok())
.and_then(|field| KdfParams::from_bytes_unvalidated(field).ok())
}
fn open_credential(root: &Path, credential_id: &str, profile: &Row) -> Opened {
match read_credential(root, credential_id) {
CorpusCredential::None => Opened::None,
CorpusCredential::Passphrase(passphrase) => Opened::Passphrase(passphrase),
CorpusCredential::PrivateKey { key, unlock } => {
let key_read = ProfileCaps::new(profile).key_read();
let bytes = read_private_key_bytes(&key, key_read.private_key_wrapped_secret_len())
.expect("read a corpus private key");
let kdf_params = match PrivateKeyHeader::from_file_bytes(&bytes) {
Ok(header) => header.kdf_params,
Err(refusal) => return Opened::RefusedKey(format!("{refusal:?}")),
};
let any_cost = KdfLimit::new(u32::MAX)
.max_time_cost(u32::MAX)
.max_lanes(u32::MAX)
.max_work(u64::MAX);
let secret = match x25519::open_x25519_private_key(
&key,
&unlock,
Some(&any_cost),
key_read,
&|_| {},
) {
Ok(secret) => Some(secret),
Err(CryptoError::KeyFileUnlockFailed) => None,
Err(refusal) => return Opened::RefusedKey(format!("{refusal:?}")),
};
Opened::PrivateKey { kdf_params, secret }
}
}
}
fn try_body(opened: &Opened, body: &[u8], kdf_limit: &KdfLimit) -> Tried {
let unwrapped = match opened {
Opened::Passphrase(passphrase) => {
let body = body.try_into().expect("a body of the native length");
argon2id::unwrap(body, passphrase, Some(kdf_limit), &|_| {})
}
Opened::PrivateKey {
secret: Some(secret),
..
} => {
let body = body.try_into().expect("a body of the native length");
x25519::unwrap(body, secret)
}
Opened::None | Opened::PrivateKey { secret: None, .. } | Opened::RefusedKey(_) => {
panic!("only an opened credential tries a body")
}
};
match unwrapped {
Ok(file_key) => Tried::Opened(Zeroizing::new(*file_key.expose())),
Err(CryptoError::RecipientUnwrapFailed { .. }) => Tried::Refused,
Err(CryptoError::InvalidFormat(FormatDefect::MalformedRecipientEntry)) => {
Tried::ZeroSharedSecret
}
Err(other) => panic!("trying a body: {other:?}"),
}
}
fn credential_breaks(
credentials: &mut Credentials,
credential_id: &str,
profile: &Row,
bytes: &[u8],
walk: &Walk,
cap_broken: bool,
) -> Decided {
let Credentials {
root,
opened,
tried,
} = credentials;
let profile_caps = ProfileCaps::new(profile);
let key_read = profile_caps.key_read();
let opened = opened
.entry((
credential_id.to_string(),
key_read.recipient_string_chars(),
key_read.private_key_wrapped_secret_len(),
))
.or_insert_with(|| open_credential(root, credential_id, profile));
let kdf_limit = profile_caps.kdf();
let every_entry_tried_first = |native: NativeRecipientType| {
walk.complete
&& !walk.entries.is_empty()
&& walk.entries.iter().all(|entry| {
entry.type_name.as_deref() == Some(native.type_name().as_bytes())
&& entry.flags == 0
&& triable(native, entry).is_some()
})
};
let mut broken = Breaks::default();
let (native, unwrap_check) = match opened {
Opened::None => return Decided::Breaks(broken),
Opened::Passphrase(_) => (NativeRecipientType::Argon2id, PassphraseUnwrap),
Opened::RefusedKey(refusal) => return Decided::KeyRefused(refusal.clone()),
Opened::PrivateKey { kdf_params, .. } if over_kdf_cap(*kdf_params, &kdf_limit) => {
return Decided::KeyRefused(format!(
"its KDF parameters are above the caps of {}",
field(profile, "limit_profile_id")
));
}
Opened::PrivateKey { secret: None, .. } => {
let must_try = walk.entries.iter().any(|entry| {
entry.type_name.as_deref() == Some(x25519::TYPE_NAME.as_bytes())
&& entry.flags == 0
&& triable(NativeRecipientType::X25519, entry).is_some()
});
if must_try {
broken.certain.insert((KeyUnlock, None));
} else {
broken.uncertain.insert((KeyUnlock, None));
}
if every_entry_tried_first(NativeRecipientType::X25519) {
broken.first_try.insert(KeyUnlock);
}
return Decided::Breaks(broken);
}
Opened::PrivateKey { .. } => (NativeRecipientType::X25519, KeyUnwrap),
};
let of_type: Vec<&Entry> = walk
.entries
.iter()
.filter(|entry| entry.type_name.as_deref() == Some(native.type_name().as_bytes()))
.collect();
let mut every_entry_tried = !of_type.is_empty();
let mut met: Vec<(Fcr, bool)> = Vec::new();
let mut within_caps: Vec<(&[u8], bool)> = Vec::new();
let ephemeral_key = x25519::EPHEMERAL_PUBLIC_KEY_OFFSET
..x25519::EPHEMERAL_PUBLIC_KEY_OFFSET + x25519::PUBLIC_KEY_SIZE;
for entry in of_type {
let skippable = entry.flags != 0;
let Some(body) = triable(native, entry) else {
every_entry_tried = false;
let doubtful = match native {
NativeRecipientType::Argon2id => stored_kdf_params(&entry.body)
.is_some_and(|params| over_kdf_cap(params, &kdf_limit))
.then_some(KdfCap),
NativeRecipientType::X25519 => entry
.body
.get(ephemeral_key.clone())
.is_some_and(|key| {
let mut padded = vec![0; native.body_len()];
padded[ephemeral_key.clone()].copy_from_slice(key);
native.validate_body(&padded).is_err()
|| matches!(
tried
.entry((credential_id.to_string(), padded.clone()))
.or_insert_with(|| try_body(opened, &padded, &kdf_limit)),
Tried::ZeroSharedSecret
)
})
.then_some(KeyAgreement),
};
broken.uncertain.extend(doubtful.map(|check| (check, None)));
continue;
};
if skippable {
every_entry_tried = false;
}
let over_cap = native == NativeRecipientType::Argon2id
&& stored_kdf_params(body).is_some_and(|params| over_kdf_cap(params, &kdf_limit));
if over_cap {
met.push((KdfCap, skippable));
every_entry_tried = false;
continue;
}
within_caps.push((body, skippable));
}
let mut candidates = Vec::new();
let mut zero_on_every_try = true;
if native == NativeRecipientType::X25519 || cap_broken || !met.is_empty() {
for (body, skippable) in within_caps {
let outcome = tried
.entry((credential_id.to_string(), body.to_vec()))
.or_insert_with(|| try_body(opened, body, &kdf_limit));
zero_on_every_try &= matches!(outcome, Tried::ZeroSharedSecret);
match outcome {
Tried::Opened(file_key) => {
candidates.push(FileKey::from_bytes_for_tests(**file_key))
}
Tried::Refused => {}
Tried::ZeroSharedSecret => met.push((KeyAgreement, skippable)),
}
}
}
let mac = header_mac_verifies(bytes, &candidates);
let verified = mac == Some(true);
for (check, skippable) in met {
if skippable || verified {
broken.uncertain.insert((check, None));
} else {
broken.certain.insert((check, None));
}
}
if cap_broken && every_entry_tried {
if candidates.is_empty() {
broken.certain.insert((unwrap_check, None));
} else if mac == Some(false) {
broken.certain.insert((HeaderMac, None));
}
}
if native == NativeRecipientType::X25519 && zero_on_every_try && every_entry_tried_first(native)
{
broken.first_try.insert(KeyAgreement);
}
Decided::Breaks(broken)
}
fn triable(native: NativeRecipientType, entry: &Entry) -> Option<&[u8]> {
entry
.whole_body()
.filter(|body| body.len() == native.body_len() && native.validate_body(body).is_ok())
}
fn header_mac_verifies(bytes: &[u8], candidates: &[FileKey]) -> Option<bool> {
let header_end = PREFIX_SIZE + read_u32_be(bytes, PREFIX_HEADER_LEN_OFFSET).ok()? as usize;
let prefix: &[u8; PREFIX_SIZE] = bytes.get(..PREFIX_SIZE)?.try_into().ok()?;
let header = bytes.get(PREFIX_SIZE..header_end)?;
let tag: &[u8; HEADER_MAC_SIZE] = bytes
.get(header_end..header_end + HEADER_MAC_SIZE)?
.try_into()
.ok()?;
let stream_nonce: &[u8; STREAM_NONCE_SIZE] = header
.get(HEADER_FIXED_STREAM_NONCE_OFFSET..HEADER_FIXED_SIZE)?
.try_into()
.ok()?;
Some(candidates.iter().any(|file_key| {
let subkeys = derive_subkeys(file_key, stream_nonce).expect("derive subkeys");
verify_header_mac(prefix, header, &subkeys.header_key, tag).is_ok()
}))
}
const KEPT_BY_NEWER_VERSIONS: [Fcr; 2] = [Magic, Kind];
fn case_breaks(
credentials: &mut Credentials,
row: &Row,
bytes: &[u8],
profile: &Row,
capabilities: &Capabilities,
) -> (Breaks<Fcr>, BTreeSet<Vec<u8>>) {
let newer_version = bytes
.get(PREFIX_VERSION_OFFSET)
.is_some_and(|version| capabilities.outer_versions.contains(version));
let mut walked = BTreeSet::new();
let breaks = Breaks::merge(Reading::all().map(|reading| {
let (mut breaks, walk) = structural_breaks(bytes, profile, capabilities, reading);
walked.extend(
walk.entries
.iter()
.filter_map(|entry| entry.type_name.clone()),
);
assert!(
walk.entries.iter().all(|entry| {
entry
.type_name
.as_deref()
.is_none_or(|name| native_type(name).is_some() || !capabilities.supports(name))
}),
"{}: a reader that declares a recipient type tries entries of that type, \
which this check list cannot evaluate",
field(row, "case_id")
);
let cap_broken = breaks.certain.contains(&(HeaderMacWorkCap, None));
let key_refusal = match credential_breaks(
credentials,
field(row, "credential_id"),
profile,
bytes,
&walk,
cap_broken,
) {
Decided::Breaks(credential) => {
breaks.certain.extend(credential.certain);
breaks.uncertain.extend(credential.uncertain);
breaks.first_try.extend(credential.first_try);
None
}
Decided::KeyRefused(refusal) => Some(refusal),
};
if newer_version {
breaks.as_newer_version(&KEPT_BY_NEWER_VERSIONS);
}
if let Some(refusal) = &key_refusal {
assert!(
breaks.certain.is_empty() && breaks.uncertain.is_empty(),
"{}: the reader refuses the key ({refusal}), and some reader breaks a check \
of the file that refusal is not ordered against",
field(row, "case_id")
);
}
if reading == Reading::SPECIFICATION {
let certain: Vec<Break> = breaks.certain.iter().copied().collect();
let report = FcrList::specification_report(&certain);
let class = field(row, "diagnostic_class");
assert!(
report == Some(class)
|| (report.is_none()
&& (newer_version || library_accepts_header(bytes, profile))),
"{}: the specification's reader reports {report:?}",
field(row, "case_id")
);
}
breaks
}));
(breaks, walked)
}
fn library_accepts_header(bytes: &[u8], profile: &Row) -> bool {
let limits = ProfileCaps::new(profile).header();
let mut reader = bytes;
crate::container::read_encrypted_header(&mut reader, limits)
.and_then(|parsed| crate::protocol::classify_recipients_within_limits(&parsed, limits))
.is_ok()
}
fn loaded_cases() -> &'static [LoadedCase<Fcr>] {
static CASES: OnceLock<Vec<LoadedCase<Fcr>>> = OnceLock::new();
CASES.get_or_init(|| {
let root = corpus_root().expect("the corpus is on disk");
let profiles = limit_profiles(&root);
let cases = rejected_cases(&root, "fcr_decrypt");
let runs = Capabilities::runs(cases.iter().map(|(row, _)| row));
let mut credentials = Credentials {
root,
opened: BTreeMap::new(),
tried: BTreeMap::new(),
};
let mut loaded = Vec::new();
for (row, bytes) in cases {
let profile = &profiles[field(&row, "limit_profile_id")];
let (base, walked) = case_breaks(
&mut credentials,
&row,
&bytes,
profile,
&Capabilities::default(),
);
let mut breaks = BTreeMap::new();
for run in &runs {
if run
.iter()
.any(|capability| capability == field(&row, "capability_id"))
{
continue;
}
let (capabilities, changed) = Capabilities::declaring(run);
let touched = !changed.is_empty()
&& (capabilities
.outer_versions
.iter()
.any(|version| bytes.get(PREFIX_VERSION_OFFSET) == Some(version))
|| capabilities
.recipient_types
.iter()
.any(|type_name| walked.contains(type_name.as_bytes())));
let run_breaks = if touched {
case_breaks(&mut credentials, &row, &bytes, profile, &capabilities).0
} else {
base.clone()
};
breaks.insert(run.clone(), run_breaks);
}
loaded.push(LoadedCase { row, breaks });
}
loaded
})
}
fn loaded_runs() -> Vec<Vec<String>> {
Capabilities::runs(loaded_cases().iter().map(|case| &case.row))
}
fn evidence(run: &[String]) -> Vec<Evidence<Fcr>> {
super::evidence::<FcrList>(loaded_cases(), run)
}
#[test]
fn the_corpus_fixes_every_claimed_fcr_check_order() {
if corpus_root().is_none() {
return;
}
for run in loaded_runs() {
let (_, changed) = Capabilities::declaring(&run);
let lost = open_pairs::<FcrList>(&evidence(&run));
let changed: Vec<Fcr> = changed.into_iter().collect();
let stray = stray_losses(&lost, &changed);
assert!(
stray.is_empty(),
"a reader that declares {run:?} finds these `.fcr` check pairs open: {stray:?}"
);
}
}
#[test]
fn leaving_out_the_witnesses_reopens_their_pair() {
if corpus_root().is_none() {
return;
}
let base = evidence(&[]);
let witnessed: [(&[&str], (Fcr, Fcr)); 12] = [
(&["prefix-order-length-before-magic"], (PrefixShort, Magic)),
(
&["entry-order-entry-header-before-flags"],
(EntryHeaderShort, FlagsReserved),
),
(
&["recipient-order-unknown-critical-before-native-flags-x25519-first"],
(UnknownCritical, NativeFlags),
),
(
&["recipient-order-unknown-critical-before-native-flags-unknown-first"],
(UnknownCritical, NativeFlags),
),
(
&["recipient-order-unknown-critical-before-native-length-x25519-first"],
(UnknownCritical, NativeLength),
),
(
&["recipient-order-unknown-critical-before-native-length-unknown-first"],
(UnknownCritical, NativeLength),
),
(
&["header-mac-work-order-mixing-before-cap"],
(Mixing, HeaderMacWorkCap),
),
(
&["header-mac-work-order-cap-before-passphrase-unwrap"],
(HeaderMacWorkCap, PassphraseUnwrap),
),
(
&["header-mac-work-order-cap-before-private-key-unlock"],
(HeaderMacWorkCap, KeyUnlock),
),
(
&["header-mac-work-order-cap-before-x25519-shared-secret"],
(HeaderMacWorkCap, KeyAgreement),
),
(
&[
"header-mac-work-order-cap-before-x25519-unwrap",
"header-mac-work-order-cap-before-x25519-shared-secret",
],
(HeaderMacWorkCap, KeyUnwrap),
),
(
&["header-mac-work-order-cap-before-header-mac"],
(HeaderMacWorkCap, HeaderMac),
),
];
for (witnesses, pair) in witnessed {
assert!(
reopens::<FcrList>(&base, witnesses, pair),
"without {witnesses:?}, {pair:?} should be open"
);
}
}
const MADE_SO_FAR: [Fcr; 2] = [Mixing, HeaderMacWorkCap];
const MET_ON_ONE_TRY: [Fcr; 2] = [KeyUnlock, KeyAgreement];
#[test]
fn the_corpus_fixes_the_checks_made_on_the_entries_met_so_far() {
if corpus_root().is_none() {
return;
}
for run in loaded_runs() {
let (_, changed) = Capabilities::declaring(&run);
let stray: Vec<(Fcr, Fcr, Direction)> = unshown_walk_orders(&run, |_| true)
.into_iter()
.filter(|(earlier, later, _)| !changed.contains(earlier) && !changed.contains(later))
.collect();
assert!(
stray.is_empty(),
"for a reader that declares {run:?}, no case rules out meeting the later check \
first: {stray:?}"
);
}
let walk_order =
|case: &LoadedCase<Fcr>| field(&case.row, "case_id").starts_with(WALK_ORDER_CASE_PREFIX);
assert!(!unshown_walk_orders(&[], |case| !walk_order(case)).is_empty());
}
fn unshown_walk_orders(
run: &[String],
kept: impl Fn(&LoadedCase<Fcr>) -> bool,
) -> Vec<(Fcr, Fcr, Direction)> {
let cases: Vec<(&LoadedCase<Fcr>, &Breaks<Fcr>)> = loaded_cases()
.iter()
.filter(|case| kept(case))
.filter_map(|case| Some((case, case.breaks.get(run)?)))
.collect();
let mut unshown = Vec::new();
for (earlier, later) in super::claimed_pairs::<FcrList>() {
if !MADE_SO_FAR.contains(&later) {
continue;
}
for &direction in walk_directions(earlier) {
let shown = cases
.iter()
.any(|(case, breaks)| shows_walk_order(case, breaks, earlier, later, direction));
if !shown {
unshown.push((earlier, later, direction));
}
}
}
unshown
}
fn walk_directions(check: Fcr) -> &'static [Direction] {
if MADE_SO_FAR.contains(&check) {
&Direction::BOTH
} else if pass(check).is_some() {
FcrList::directions(FcrList::direction_group(check))
} else if check == TrailingBytes {
&[Direction::FrontToBack]
} else {
&[]
}
}
fn shows_walk_order(
case: &LoadedCase<Fcr>,
breaks: &Breaks<Fcr>,
earlier: Fcr,
later: Fcr,
direction: Direction,
) -> bool {
let class = FcrList::class(earlier);
if field(&case.row, "diagnostic_class") != class
|| !breaks.certain.iter().any(|(check, _)| *check == earlier)
{
return false;
}
let Some(met) = breaks
.reach
.get(&later)
.and_then(|reach| reach.toward(direction).last)
else {
return false;
};
let of_class: Vec<Break> = breaks
.certain
.iter()
.chain(&breaks.uncertain)
.copied()
.filter(|(check, _)| FcrList::class(*check) == class)
.collect();
let met_after = |check: Fcr| {
if check == TrailingBytes {
return direction == Direction::FrontToBack;
}
let first = match breaks.reach.get(&check) {
Some(reach) => Some(reach.toward(direction).first),
None => {
let entries = of_class
.iter()
.filter(|(broken, _)| *broken == check)
.filter_map(|(_, entry)| *entry);
match direction {
Direction::FrontToBack => entries.min(),
Direction::BackToFront => entries.max(),
}
}
};
first.is_some_and(|first| direction.meets_before(met, first))
};
let in_walk = pass(earlier) == Some(Pass::Framing) || earlier == TrailingBytes;
of_class
.iter()
.all(|&(check, _)| (check == earlier || in_walk) && met_after(check))
}
#[test]
fn the_corpus_fixes_the_work_cap_before_the_entry_tried_first() {
if corpus_root().is_none() {
return;
}
let claimed = super::claimed_pairs::<FcrList>();
for later in MET_ON_ONE_TRY {
assert!(claimed.contains(&(HeaderMacWorkCap, later)));
}
for run in loaded_runs() {
let (_, changed) = Capabilities::declaring(&run);
let over_cap: Vec<&Breaks<Fcr>> = loaded_cases()
.iter()
.filter(|case| field(&case.row, "diagnostic_class") == FcrList::class(HeaderMacWorkCap))
.filter_map(|case| case.breaks.get(&run))
.collect();
for later in MET_ON_ONE_TRY {
if changed.contains(&HeaderMacWorkCap) || changed.contains(&later) {
continue;
}
assert!(
over_cap
.iter()
.any(|breaks| shows_cap_before_first_try(breaks, later)),
"for a reader that declares {run:?}, no case rules out meeting {later:?} on the \
entry tried first before the work cap"
);
}
}
}
fn shows_cap_before_first_try(breaks: &Breaks<Fcr>, later: Fcr) -> bool {
let class = FcrList::class(HeaderMacWorkCap);
let first_within = breaks
.reach
.get(&HeaderMacWorkCap)
.is_some_and(|reach| reach.front_to_back.first > 0);
breaks.certain.contains(&(HeaderMacWorkCap, None))
&& breaks.first_try.contains(&later)
&& first_within
&& breaks
.certain
.iter()
.chain(&breaks.uncertain)
.all(|(check, _)| *check == HeaderMacWorkCap || FcrList::class(*check) != class)
}
#[test]
fn the_mixing_rules_stay_broken_as_more_entries_are_met() {
const LONGEST: usize = 4;
let kinds: Vec<RecipientEntry> = NATIVE_TYPES
.iter()
.map(|native| native.type_name())
.chain([""])
.map(entry_of_type)
.collect();
let mut lists = vec![Vec::new()];
for _ in 0..LONGEST {
lists = lists
.iter()
.flat_map(|list: &Vec<RecipientEntry>| {
kinds
.iter()
.map(move |kind| [list.clone(), vec![kind.clone()]].concat())
})
.collect();
}
assert!(lists.iter().any(|list| mixing_refused(list.clone(), None)));
for list in &lists {
for declared in [None].into_iter().chain((1..=2 * LONGEST).map(Some)) {
for met in 1..LONGEST {
let refused = |count: usize| mixing_refused(list[..count].to_vec(), declared);
assert!(
!refused(met) || refused(met + 1),
"the mixing rules refuse {:?} but not one more entry",
&list[..met]
);
}
}
}
}
#[test]
fn the_corpus_fixes_the_fcr_per_entry_passes() {
if corpus_root().is_none() {
return;
}
let units = units();
let spec = specification_reader(&units);
let files = small_files(&units);
let readers = readers(&units);
assert!(readers.iter().any(|reader| {
files
.iter()
.any(|file| report(reader, &units, file) != report(&spec, &units, file))
}));
let base = per_entry_cases(&[]);
for run in loaded_runs() {
let cases = per_entry_cases(&run);
if !run.is_empty() && cases == base {
continue;
}
let passes_every_case = |reader: &PassReader| {
cases
.iter()
.all(|(instance, class)| report(reader, &units, instance) == Some(class.as_str()))
};
assert!(
passes_every_case(&spec),
"the specification's reader reports every stored class"
);
let (_, changed) = Capabilities::declaring(&run);
let mut gaps = BTreeMap::new();
for reader in readers.iter().filter(|reader| passes_every_case(reader)) {
for file in &files {
let lost = file.iter().any(|(check, _)| changed.contains(check));
if !lost && report(reader, &units, file) != report(&spec, &units, file) {
gaps.entry(file.iter().copied().collect::<Vec<_>>())
.or_insert_with(|| reader.clone());
}
}
}
assert!(
gaps.is_empty(),
"for a reader that declares {run:?}, readers that pass every case disagree on: \
{gaps:?}"
);
}
}
fn per_entry_cases(run: &[String]) -> Vec<(BTreeSet<(Fcr, usize)>, String)> {
let position = |check: Fcr| {
FcrList::order()
.iter()
.position(|c| *c == check)
.expect("every check is in the order")
};
let last_per_entry = FcrList::order()
.iter()
.rposition(|check| pass(*check).is_some())
.expect("a per-entry check");
evidence(run)
.into_iter()
.filter(|evidence| {
evidence
.broken
.iter()
.all(|(check, entry)| entry.is_some() || position(*check) > last_per_entry)
})
.map(|evidence| {
let instance: BTreeSet<(Fcr, usize)> = evidence
.broken
.iter()
.filter_map(|(check, entry)| entry.map(|entry| (*check, entry)))
.collect();
(instance, evidence.class)
})
.filter(|(instance, _)| !instance.is_empty())
.collect()
}
fn units() -> Vec<Vec<Fcr>> {
let per_entry = FcrList::order()
.iter()
.copied()
.filter(|check| pass(*check).is_some());
let mut units: Vec<Vec<Fcr>> = Vec::new();
for check in per_entry {
match units.last_mut() {
Some(unit) if FRAMING.contains(&check) && unit.iter().all(|c| FRAMING.contains(c)) => {
unit.push(check)
}
_ => units.push(vec![check]),
}
}
units
}
#[derive(Clone, Debug)]
struct PassReader {
passes: Vec<(Vec<usize>, Direction)>,
}
fn specification_reader(units: &[Vec<Fcr>]) -> PassReader {
let indices: Vec<usize> = (0..units.len()).collect();
PassReader {
passes: indices
.chunk_by(|&a, &b| pass(units[a][0]) == pass(units[b][0]))
.map(|members| (members.to_vec(), Direction::FrontToBack))
.collect(),
}
}
fn report(
reader: &PassReader,
units: &[Vec<Fcr>],
instance: &BTreeSet<(Fcr, usize)>,
) -> Option<&'static str> {
let entries: BTreeSet<usize> = instance.iter().map(|(_, entry)| *entry).collect();
for (members, direction) in &reader.passes {
let order: Vec<usize> = match direction {
Direction::FrontToBack => entries.iter().copied().collect(),
Direction::BackToFront => entries.iter().rev().copied().collect(),
};
for entry in order {
for &member in members {
for &check in &units[member] {
if instance.contains(&(check, entry)) {
return Some(FcrList::class(check));
}
}
}
}
}
None
}
fn readers(units: &[Vec<Fcr>]) -> Vec<PassReader> {
fn partitions(items: &[usize]) -> Vec<Vec<Vec<usize>>> {
let Some((&first, rest)) = items.split_first() else {
return vec![Vec::new()];
};
let mut out = Vec::new();
for partition in partitions(rest) {
for block in 0..partition.len() {
for position in 0..=partition[block].len() {
let mut next = partition.clone();
next[block].insert(position, first);
out.push(next);
}
}
for block in 0..=partition.len() {
let mut next = partition.clone();
next.insert(block, vec![first]);
out.push(next);
}
}
out
}
let allows = |members: &[usize], direction: Direction| {
members.iter().all(|&member| {
FcrList::directions(FcrList::direction_group(units[member][0])).contains(&direction)
})
};
let indices: Vec<usize> = (0..units.len()).collect();
let mut readers = Vec::new();
for partition in partitions(&indices) {
for directions in 0..1u32 << partition.len() {
let passes: Vec<(Vec<usize>, Direction)> = partition
.iter()
.enumerate()
.map(|(index, members)| {
let direction = if directions & (1 << index) == 0 {
Direction::FrontToBack
} else {
Direction::BackToFront
};
(members.clone(), direction)
})
.collect();
if passes
.iter()
.all(|(members, direction)| allows(members, *direction))
{
readers.push(PassReader { passes });
}
}
}
readers
}
fn small_files(units: &[Vec<Fcr>]) -> Vec<BTreeSet<(Fcr, usize)>> {
let per_entry: Vec<Fcr> = units.iter().flatten().copied().collect();
let stops_the_walk = [EntryHeaderShort, BodyLenMax, EntryFit];
let mut files = Vec::new();
for &a in &per_entry {
files.push(BTreeSet::from([(a, 0)]));
for &b in &per_entry {
if a < b && one_entry_breaks_both(a, b) {
files.push(BTreeSet::from([(a, 0), (b, 0)]));
}
if !stops_the_walk.contains(&a) {
files.push(BTreeSet::from([(a, 0), (b, 1)]));
}
}
}
files
}