🦀 bions-rust vague 1 : 6 briques build-your-own-x — on ne les rebuild plus jamais

Principe RS-7 : « dès qu'on build un truc, plus personne n'a à le rebuild —
la seule chose à faire est l'optimisation. » (nexus/RepoVerse)
- bion-vc      : horloges vectorielles + MvReg fork-visible (LA spec
                 xion-relativiste-v0 enfin codée — CRDT testé par permutations)
- bion-triplet : l'Adressage Génératif (gen_hash BLAKE3, coords, résidu ;
                 résidu vide quand déjà-su ; align décidable au bit)
- bion-tsoinlog: journal append-only rejouable (CRC32 maison, crash-recovery)
- bion-kv      : magasin clé-valeur bitcask (compaction atomique, tombstones)
- bion-regex   : moteur Thompson NFA linéaire (jamais exponentiel — Russ Cox)
- bion-git     : mini-git content-addressed (SHA-1 maison + vecteurs officiels,
                 branches divergentes = le fork visible)
129 tests verts, clippy 0 warning, doc française = chaque bion est un cours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cloudion-labo
2026-08-16 01:07:31 +00:00
commit 2556698dd3
27 changed files with 6518 additions and 0 deletions

11
bion-triplet/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "bion-triplet"
version = "0.1.0"
edition = "2021"
description = "Le triplet d'Adressage Génératif : (hash_générateur, coordonnées, hash_résidu). Une donnée = une adresse dans un espace génératif."
license = "AGPL-3.0"
[dependencies]
# EXCEPTION std-only, explicitement autorisée : on n'implémente pas BLAKE3
# from scratch — le bion est la LOGIQUE du triplet, pas la fonction de hachage.
blake3 = { version = "1", default-features = false }

106
bion-triplet/README.md Normal file
View File

@@ -0,0 +1,106 @@
# bion-triplet
Le **triplet d'Adressage Génératif** en Rust `std`-only : la donnée comme pure adresse.
```
triplet = ( hash_générateur , coordonnées , hash_résidu )
└─ BLAKE3, 32 o ─┘ └─ opaque ──┘ └─ BLAKE3, 32 o ─┘
```
## Quoi
Ce crate fige la **logique** de la spec `adressage-generatif-triplet-v0`
(my_website2, `resources/papers/adressage-generatif-triplet-v0.md`) : les trois
champs du triplet et les trois opérations dessus.
- **ÉCRIRE** — `encode(générateur, data, coords)` : calcule
`résidu = data ⊖ G(coords)` et retourne `(Triplet, résidu)`. On ne garde que
l'écart au prédictible ; si la donnée est exactement la prédiction, le résidu
est **vide** (le cas « déjà su » : la pure structure ne coûte rien).
- **LIRE** — `decode(générateur, triplet, résidu)` : vérifie les **deux** hash
(générateur ET résidu), puis `data = G(coords) ⊕ résidu`. Bit-exact, sur
toute machine, à toute époque.
- **ALIGNEMENT** — `align(a, b)` : égalité des `gen_hash`, décidable au bit.
Deux nœuds alignés ne s'échangent que `(coordonnées, résidu)` — des adresses,
pas des blobs.
Générateurs d'exemple fournis : `ConstGenerator` (octets constants),
`PatternGenerator` (motif répété + offset), `IdentityGenerator` (le plancher :
résidu = donnée entière, le cas dégénéré posé par la spec).
## Pourquoi
Une donnée n'est pas un contenu qu'on garde, c'est une adresse dans un espace
génératif. Le triplet est le « connecteur logiciel » éternel de XERB0XI0N :
position, DM et réplication sont la même opération à trois échelles — ce qui
voyage n'est jamais l'instant, c'est **l'écart entre l'instant et ce que
l'autre bout savait déjà en prédire**. Ce bion se build une fois ; ensuite on
n'optimise que les générateurs au-dessus, jamais le connecteur.
Il **complète** `tsoin-codec` (xerboxion-rt) sans le dupliquer : `tsoin-codec`
est un *compresseur* (codage arithmétique, une manière maline de fabriquer un
petit résidu) ; `bion-triplet` est le *format d'adresse* qui cite un générateur
et un résidu par hash. Le codec se branche au-dessus, comme n'importe quel `⊖`
de domaine.
## Exemple
```rust
use bion_triplet::{encode, decode, align, ConstGenerator};
let g = ConstGenerator::new(0x2A);
let data = [0x2A, 0x2A, 0xFF, 0x2A];
let (triplet, residu) = encode(&g, &data, ConstGenerator::coords(4));
assert_eq!(residu, vec![0, 0, 0x2A ^ 0xFF, 0]); // seule la surprise pèse
assert_eq!(decode(&g, &triplet, &residu).unwrap(), data);
let (autre, _) = encode(&g, b"****", ConstGenerator::coords(4));
assert!(align(&triplet, &autre)); // même générateur, décidable au bit
```
## Choix de design (documentés)
- **`⊖` = XOR à longueur portée par le résidu.** Sans ambiguïté : résidu vide
⇔ « exactement la prédiction » ; sinon `résidu.len() == data.len()` et la
prédiction est complétée par des zéros si elle est plus courte. Honnête face
au mur de Kolmogorov : un mauvais générateur donne un résidu au poids plein,
jamais un mensonge.
- **Décoder vérifie avant de calculer.** Mauvais générateur ⇒
`GeneratorMismatch` ; résidu corrompu ⇒ `ResidualMismatch`. Jamais une donnée
silencieusement fausse.
- **Identité d'un générateur = hash d'une description canonique versionnée**
(`bion-triplet:v0:const:…`), jamais un état interne. Changer la sémantique =
nouveau nom = nouveau hash : rétrocompatibilité éternelle.
- **Un générateur ne panique jamais** sur des coordonnées malformées : il
prédit « rien » et le résidu porte tout le réel.
- **Exception dépendance** : `blake3` (default-features off). On n'implémente
pas une fonction de hachage cryptographique from scratch — le bion, c'est la
logique du triplet.
## Liens build-your-own-x
Dans l'esprit de [build-your-own-x](https://github.com/codecrafters-io/build-your-own-x) :
- *Build your own Git* — le store adressé par contenu (`C1`) que le triplet
cite est exactement le modèle objet de Git (hash → blob).
- *Build your own Docker / virtual machine* — le générateur comme programme pur
sandboxé (WASM `C0`) rejouable partout.
- Compression / codage arithmétique (voir `tsoin-codec` du core) — le `⊖`
malin qui fabrique de petits résidus.
## Comment l'optimiser (l'invitation au fork)
Le triplet ne change plus ; **tout le gain est au-dessus**. Pistes :
- un `⊖` de domaine plus malin que le XOR (diff structuré, delta d'images,
codage arithmétique via `tsoin-codec`) — même API, résidu plus petit ;
- des générateurs réels : modèle de mouvement GPS, contexte de DM, index
Hilbert 4D du Cubion — tous bit-exacts (entiers seulement) ;
- une sérialisation binaire du `Triplet` (72 octets + coords) pour le manifeste
de réplication diff-only (*XI0N-résidu*) ;
- un panel de générateurs nommés + la grandeur honnête
`certitude = 1 L_min / L_baseline` (spec §3.2) ;
- brancher le chiffrement GPG du résidu (toujours, au repos et en transit) —
hors de ce crate, dans la couche réseau.
Licence AGPL-3.0 · spec gelée `adressage-generatif-triplet-v0` · *Ne pas nuire.*

542
bion-triplet/src/lib.rs Normal file
View File

@@ -0,0 +1,542 @@
//! `bion-triplet` — le triplet d'Adressage Génératif.
//!
//! # Le cours en trois phrases
//!
//! Une donnée n'est pas un contenu qu'on garde, c'est une **adresse** dans un espace
//! génératif : un point qu'on retrouve. L'adresse est un **triplet**
//! `(hash_générateur, coordonnées, hash_résidu)` : le générateur est un programme pur
//! partagé par tous (un bien commun), les coordonnées localisent la donnée *dans* ce
//! générateur, et le résidu est **l'écart au bit près** entre ce que le générateur
//! prédit et ce que le réel a fait. Seul le résidu exige du stockage : écrire, c'est
//! ne conserver que la surprise.
//!
//! Spec de référence : `adressage-generatif-triplet-v0` (my_website2,
//! `resources/papers/adressage-generatif-triplet-v0.md`). Ce crate en fige la
//! **logique** — les trois champs et les trois opérations — en Rust `std`-only
//! (seule exception : le crate `blake3`, car on ne réimplémente pas une fonction
//! de hachage cryptographique ; le bion, c'est le triplet, pas le hash).
//!
//! # Les trois opérations
//!
//! - **ÉCRIRE** — [`encode`] : choisir un générateur `G`, calculer
//! `résidu = donnée ⊖ G(coordonnées)`, ne garder que le triplet + le résidu.
//! - **LIRE** — [`decode`] : `donnée = G(coordonnées) ⊕ résidu`, après vérification
//! des **deux** hash (générateur ET résidu). Déterministe, bit pour bit.
//! - **ALIGNEMENT** — [`align`] : deux triplets partagent un générateur ssi leurs
//! `gen_hash` sont égaux. Une égalité de hash, **décidable au bit** — pas une
//! synchronisation floue.
//!
//! # Le choix de `⊖` / `⊕` (documenté, cas dégénéré de la spec)
//!
//! La spec laisse l'opérateur de recombinaison au domaine ; ce crate implémente le
//! cas dégénéré **XOR à longueur portée par le résidu**, avec un raffinement :
//!
//! - si `donnée == prédiction` octet pour octet, le résidu est **vide** (`[]`).
//! C'est le cas « déjà su » : la pure structure, zéro stockage (spec §1,
//! « résidu = vide ⇒ pure structure ») ;
//! - sinon `résidu.len() == donnée.len()` et `résidu[i] = donnée[i] XOR pred[i]`,
//! où la prédiction est **complétée par des zéros** si elle est plus courte que la
//! donnée (au-delà de la prédiction, le résidu porte la donnée telle quelle —
//! c'est le cas dégénéré « générateur = identité ⇒ résidu = la donnée entière »).
//!
//! Ce schéma est **sans ambiguïté** : un résidu vide signifie exactement « la
//! prédiction entière », un résidu non vide fixe la longueur de la donnée. Il est
//! honnête vis-à-vis du mur de Kolmogorov : un mauvais générateur donne un résidu
//! aussi lourd que la donnée, jamais plus léger que la surprise réelle. Un `⊖` plus
//! malin (diff structuré, codage arithmétique — voir `tsoin-codec` dans
//! xerboxion-rt) se branche *au-dessus*, en compressant le résidu ; le triplet, lui,
//! ne change pas : c'est le connecteur éternel.
//!
//! # Exemple
//!
//! ```
//! use bion_triplet::{encode, decode, align, ConstGenerator, Generator};
//!
//! // Un générateur commun : « que des octets 0x2A ».
//! let g = ConstGenerator::new(0x2A);
//! // Les coordonnées (opaques pour le triplet) : ici, la longueur attendue.
//! let coords = ConstGenerator::coords(4);
//!
//! // ÉCRIRE : la donnée diffère de la prédiction sur un seul octet.
//! let data = [0x2A, 0x2A, 0xFF, 0x2A];
//! let (triplet, residu) = encode(&g, &data, coords.clone());
//! assert_eq!(residu, vec![0, 0, 0x2A ^ 0xFF, 0]); // seule la surprise pèse
//!
//! // LIRE : régénérer + appliquer l'écart, hash vérifiés.
//! let lu = decode(&g, &triplet, &residu).unwrap();
//! assert_eq!(lu, data);
//!
//! // ALIGNEMENT : même générateur ⇒ on ne diffe que des adresses.
//! let (autre, _) = encode(&g, b"****", coords);
//! assert!(align(&triplet, &autre));
//! ```
/// Taille d'un hash BLAKE3, en octets. Gelée par la spec v0 (« BLAKE3, 32 o »).
pub const HASH_LEN: usize = 32;
/// Un hash BLAKE3 de 32 octets — le seul type d'identité du format éternel.
pub type Hash = [u8; HASH_LEN];
/// LE TRIPLET — la donnée comme pure adresse (spec §1, `[POSÉ]`).
///
/// Trois champs, tous de taille bornée (les coordonnées sont opaques mais
/// minuscules par construction : une par donnée). Le triplet ne contient
/// **aucun octet de contenu**, seulement des références vérifiables :
///
/// - [`gen_hash`](Self::gen_hash) — BLAKE3 du générateur (programme pur,
/// déterministe). Le générateur est un bien commun, répliqué gratuitement.
/// - [`coords`](Self::coords) — localisent la donnée *dans* le générateur
/// (seed, x/y/z/t, index Hilbert, clé, temps…). Le format interne est propre
/// au domaine du générateur ; le triplet les traite comme opaques.
/// - [`res_hash`](Self::res_hash) — BLAKE3 du résidu : l'écart au bit près
/// entre prédiction et réel. La seule part qui exige du stockage — le Tsoin.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Triplet {
/// BLAKE3 du générateur — l'identité du « déjà su » partagé.
pub gen_hash: Hash,
/// Coordonnées opaques : où, dans le générateur, vit cette donnée.
pub coords: Vec<u8>,
/// BLAKE3 du résidu — l'identité de la surprise.
pub res_hash: Hash,
}
/// Un **générateur** : un programme pur et déterministe qui, pour des
/// coordonnées données, prédit des octets — le « déjà su » commun aux deux
/// bouts.
///
/// Contrat (spec §3, garde-fou 4) : `generate` doit être **bit-exact** sur
/// toute machine, à toute époque — même entrée, mêmes octets, partout. Un
/// générateur non reproductible (floats, réseau de neurones non quantifié) ne
/// peut pas porter un `hash_générateur` du format éternel.
///
/// `hash` doit identifier le générateur de façon stable et sans collision
/// entre générateurs différents : hasher une **description canonique**
/// (nom versionné + paramètres), jamais un état interne éphémère.
pub trait Generator {
/// Prédit les octets aux coordonnées données. Pur et déterministe.
fn generate(&self, coords: &[u8]) -> Vec<u8>;
/// BLAKE3 de la description canonique du générateur.
fn hash(&self) -> Hash;
}
/// Erreurs de [`decode`] : le triplet est vérifiable, donc il peut refuser.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TripletError {
/// Le générateur fourni n'est pas celui que le triplet cite
/// (`hash_générateur` ≠ `generator.hash()`). L'ALIGNEMENT a échoué :
/// on ne « devine » jamais avec le mauvais déjà-su.
GeneratorMismatch {
/// Le hash attendu (celui du triplet).
expected: Hash,
/// Le hash du générateur effectivement fourni.
got: Hash,
},
/// Le résidu fourni n'est pas celui que le triplet cite
/// (`hash_résidu` ≠ BLAKE3(résidu)) : résidu corrompu ou falsifié.
ResidualMismatch {
/// Le hash attendu (celui du triplet).
expected: Hash,
/// Le hash du résidu effectivement fourni.
got: Hash,
},
}
impl std::fmt::Display for TripletError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn hex(h: &Hash) -> String {
h.iter().map(|b| format!("{b:02x}")).collect()
}
match self {
TripletError::GeneratorMismatch { expected, got } => write!(
f,
"générateur non aligné : le triplet cite {}, reçu {}",
hex(expected),
hex(got)
),
TripletError::ResidualMismatch { expected, got } => write!(
f,
"résidu corrompu : le triplet cite {}, reçu {}",
hex(expected),
hex(got)
),
}
}
}
impl std::error::Error for TripletError {}
/// BLAKE3 d'un bloc d'octets — le hash du format éternel.
pub fn hash_bytes(bytes: &[u8]) -> Hash {
*blake3::hash(bytes).as_bytes()
}
/// **ÉCRIRE** (spec §2) : encode `data` comme adresse dans `generator`.
///
/// Calcule `résidu = data ⊖ generator.generate(&coords)` (XOR, prédiction
/// complétée par des zéros — voir la doc du crate) et retourne le couple
/// `(Triplet, résidu)`. Le triplet est l'adresse (à répliquer partout) ;
/// le résidu est la seule part à stocker — et, dans la flotte, la seule à
/// chiffrer GPG (spec §3, garde-fou 5 ; le chiffrement vit hors de ce crate).
///
/// Cas « déjà su » : si `data` est exactement la prédiction, le résidu est
/// **vide** — la donnée est pure structure, elle ne coûte rien.
pub fn encode<G: Generator>(generator: &G, data: &[u8], coords: Vec<u8>) -> (Triplet, Vec<u8>) {
let prediction = generator.generate(&coords);
let residual = if data == prediction.as_slice() {
Vec::new()
} else {
data.iter()
.enumerate()
.map(|(i, &b)| b ^ prediction.get(i).copied().unwrap_or(0))
.collect()
};
let triplet = Triplet {
gen_hash: generator.hash(),
coords,
res_hash: hash_bytes(&residual),
};
(triplet, residual)
}
/// **LIRE** (spec §2) : `donnée = G(coordonnées) ⊕ résidu`, après vérification
/// des deux hash.
///
/// Vérifie d'abord que `generator` est bien celui que le triplet cite
/// (sinon [`TripletError::GeneratorMismatch`]), puis que `residual` est bien
/// le résidu cité (sinon [`TripletError::ResidualMismatch`]). Ensuite
/// seulement, régénère la prédiction aux coordonnées du triplet et applique
/// l'écart. Déterministe, bit pour bit, sur toute machine, à toute époque.
pub fn decode<G: Generator>(
generator: &G,
triplet: &Triplet,
residual: &[u8],
) -> Result<Vec<u8>, TripletError> {
let got_gen = generator.hash();
if got_gen != triplet.gen_hash {
return Err(TripletError::GeneratorMismatch {
expected: triplet.gen_hash,
got: got_gen,
});
}
let got_res = hash_bytes(residual);
if got_res != triplet.res_hash {
return Err(TripletError::ResidualMismatch {
expected: triplet.res_hash,
got: got_res,
});
}
let prediction = generator.generate(&triplet.coords);
if residual.is_empty() {
// Résidu vide = « exactement la prédiction » : pure structure.
return Ok(prediction);
}
Ok(residual
.iter()
.enumerate()
.map(|(i, &b)| b ^ prediction.get(i).copied().unwrap_or(0))
.collect())
}
/// **ALIGNEMENT** (spec §2) : `a` et `b` citent-ils le même générateur ?
///
/// Une simple égalité de `gen_hash` — décidable au bit, pas une
/// synchronisation floue. Deux triplets alignés ne diffèrent que par
/// `(coordonnées, hash_résidu)` : entre deux nœuds alignés, on diffe des
/// adresses au lieu de transporter des blobs. C'est le levier de la
/// réplication diff-only, du DM à résidu et de la position prédite.
pub fn align(a: &Triplet, b: &Triplet) -> bool {
a.gen_hash == b.gen_hash
}
// ---------------------------------------------------------------------------
// Générateurs d'exemple — pédagogiques, bit-exacts, format éternel respecté.
// ---------------------------------------------------------------------------
/// Préfixe de domaine des descriptions canoniques des générateurs d'exemple.
/// Versionné : changer la sémantique d'un générateur = nouveau nom = nouveau
/// hash (rétrocompatibilité éternelle — on n'écrase jamais un `gen_hash`).
const DOMAIN: &[u8] = b"bion-triplet:v0:";
/// Générateur constant : prédit `len` octets tous égaux à `value`.
///
/// Coordonnées : la longueur attendue, en `u64` little-endian (8 octets) —
/// fabriquées par [`ConstGenerator::coords`]. Des coordonnées malformées
/// (≠ 8 octets) prédisent zéro octet : un générateur ne panique jamais, il
/// prédit « rien » et laisse le résidu porter tout le réel.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConstGenerator {
value: u8,
}
impl ConstGenerator {
/// Un générateur qui prédit des octets tous égaux à `value`.
pub fn new(value: u8) -> Self {
Self { value }
}
/// Fabrique les coordonnées : la longueur attendue de la donnée.
pub fn coords(len: u64) -> Vec<u8> {
len.to_le_bytes().to_vec()
}
}
impl Generator for ConstGenerator {
fn generate(&self, coords: &[u8]) -> Vec<u8> {
let Ok(bytes) = <[u8; 8]>::try_from(coords) else {
return Vec::new();
};
let len = u64::from_le_bytes(bytes);
// Garde-fou local : ne jamais allouer plus que ce qu'un u32 adresse —
// des coordonnées hostiles ne doivent pas faire tomber le lecteur.
let len = usize::try_from(len.min(u32::MAX as u64)).unwrap_or(0);
vec![self.value; len]
}
fn hash(&self) -> Hash {
let mut desc = Vec::with_capacity(DOMAIN.len() + 6);
desc.extend_from_slice(DOMAIN);
desc.extend_from_slice(b"const:");
desc.push(self.value);
hash_bytes(&desc)
}
}
/// Générateur de motif : prédit `len` octets d'un motif répété en boucle,
/// à partir d'un décalage.
///
/// C'est le générateur « structure périodique » : tout ce qui se répète
/// (trames, en-têtes, textures, silences) s'effondre à une coordonnée.
/// Coordonnées : `(offset u64 LE, len u64 LE)` — 16 octets, fabriquées par
/// [`PatternGenerator::coords`]. Coordonnées malformées ou motif vide ⇒
/// prédiction vide (même philosophie que [`ConstGenerator`]).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatternGenerator {
pattern: Vec<u8>,
}
impl PatternGenerator {
/// Un générateur qui répète `pattern` en boucle.
pub fn new(pattern: Vec<u8>) -> Self {
Self { pattern }
}
/// Fabrique les coordonnées : décalage dans le motif + longueur attendue.
pub fn coords(offset: u64, len: u64) -> Vec<u8> {
let mut c = Vec::with_capacity(16);
c.extend_from_slice(&offset.to_le_bytes());
c.extend_from_slice(&len.to_le_bytes());
c
}
}
impl Generator for PatternGenerator {
fn generate(&self, coords: &[u8]) -> Vec<u8> {
if coords.len() != 16 || self.pattern.is_empty() {
return Vec::new();
}
let offset = u64::from_le_bytes(coords[..8].try_into().unwrap());
let len = u64::from_le_bytes(coords[8..].try_into().unwrap());
let len = usize::try_from(len.min(u32::MAX as u64)).unwrap_or(0);
let plen = self.pattern.len() as u64;
(0..len as u64)
.map(|i| self.pattern[((offset + i) % plen) as usize])
.collect()
}
fn hash(&self) -> Hash {
let mut desc = Vec::with_capacity(DOMAIN.len() + 8 + self.pattern.len());
desc.extend_from_slice(DOMAIN);
desc.extend_from_slice(b"pattern:");
desc.extend_from_slice(&self.pattern);
hash_bytes(&desc)
}
}
/// Générateur identité : ne prédit **rien**.
///
/// Le cas dégénéré `[POSÉ]` de la spec §1 : « générateur = identité ⇒
/// résidu = la donnée entière (stockage classique) ». C'est le plancher de
/// rétrocompatibilité éternelle : toute donnée, même sans aucun générateur
/// malin, a un triplet valide — son résidu pèse juste son poids plein
/// (le mur de Kolmogorov, assumé au lieu d'être caché).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct IdentityGenerator;
impl Generator for IdentityGenerator {
fn generate(&self, _coords: &[u8]) -> Vec<u8> {
Vec::new()
}
fn hash(&self) -> Hash {
let mut desc = Vec::with_capacity(DOMAIN.len() + 8);
desc.extend_from_slice(DOMAIN);
desc.extend_from_slice(b"identity");
hash_bytes(&desc)
}
}
// ---------------------------------------------------------------------------
// Tests — chaque garde-fou de la spec a le sien.
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
/// LIRE(ÉCRIRE(x)) == x, bit pour bit, avec le générateur constant.
#[test]
fn roundtrip_const() {
let g = ConstGenerator::new(0xAB);
let data = [0xAB, 0x00, 0xAB, 0xFF, 0x12];
let (t, r) = encode(&g, &data, ConstGenerator::coords(5));
assert_eq!(decode(&g, &t, &r).unwrap(), data);
}
/// Roundtrip avec le générateur de motif, décalage non nul inclus.
#[test]
fn roundtrip_pattern_avec_offset() {
let g = PatternGenerator::new(vec![1, 2, 3]);
// Prédiction attendue depuis offset=2 : 3,1,2,3,1,2,3
let data = [3, 1, 2, 9, 1, 2, 3];
let (t, r) = encode(&g, &data, PatternGenerator::coords(2, 7));
// Seul l'octet 3 (9 vs 3) est une surprise.
assert_eq!(r, vec![0, 0, 0, 9 ^ 3, 0, 0, 0]);
assert_eq!(decode(&g, &t, &r).unwrap(), data);
}
/// Le cas « déjà su » : data == prédiction ⇒ résidu VIDE, zéro stockage.
#[test]
fn residu_vide_quand_deja_su() {
let g = ConstGenerator::new(0x2A);
let data = [0x2A; 16];
let (t, r) = encode(&g, &data, ConstGenerator::coords(16));
assert!(r.is_empty(), "la pure structure ne coûte rien");
assert_eq!(decode(&g, &t, &r).unwrap(), data.to_vec());
}
/// ALIGNEMENT : même générateur ⇒ true ; générateurs différents ⇒ false.
#[test]
fn align_egalite_de_gen_hash() {
let g1 = ConstGenerator::new(1);
let g2 = ConstGenerator::new(2);
let (a, _) = encode(&g1, b"aaa", ConstGenerator::coords(3));
let (b, _) = encode(&g1, b"zzz", ConstGenerator::coords(3));
let (c, _) = encode(&g2, b"aaa", ConstGenerator::coords(3));
assert!(align(&a, &b), "même générateur, coordonnées différentes");
assert!(!align(&a, &c), "générateurs différents");
}
/// Décoder avec le mauvais générateur ⇒ GeneratorMismatch, jamais une
/// donnée silencieusement fausse.
#[test]
fn mauvais_generateur_refuse() {
let bon = ConstGenerator::new(7);
let mauvais = ConstGenerator::new(8);
let (t, r) = encode(&bon, b"hello", ConstGenerator::coords(5));
match decode(&mauvais, &t, &r) {
Err(TripletError::GeneratorMismatch { expected, got }) => {
assert_eq!(expected, bon.hash());
assert_eq!(got, mauvais.hash());
}
autre => panic!("attendu GeneratorMismatch, obtenu {autre:?}"),
}
}
/// Un résidu falsifié/corrompu ⇒ ResidualMismatch.
#[test]
fn residu_corrompu_refuse() {
let g = ConstGenerator::new(7);
let (t, mut r) = encode(&g, b"hello", ConstGenerator::coords(5));
r[0] ^= 0xFF;
assert!(matches!(
decode(&g, &t, &r),
Err(TripletError::ResidualMismatch { .. })
));
}
/// Générateur identité : le résidu EST la donnée (stockage classique,
/// cas dégénéré posé par la spec).
#[test]
fn identite_residu_est_la_donnee() {
let g = IdentityGenerator;
let data = b"le reel tout entier";
let (t, r) = encode(&g, data, Vec::new());
assert_eq!(r, data.to_vec(), "identité : résidu = donnée entière");
assert_eq!(decode(&g, &t, &r).unwrap(), data.to_vec());
}
/// Donnée PLUS LONGUE que la prédiction : au-delà, le résidu porte la
/// donnée telle quelle (XOR avec zéro). Roundtrip exact.
#[test]
fn donnee_plus_longue_que_la_prediction() {
let g = ConstGenerator::new(0x11);
let data = [0x11, 0x11, 0xAA, 0xBB]; // prédiction : 2 octets seulement
let (t, r) = encode(&g, &data, ConstGenerator::coords(2));
assert_eq!(r, vec![0, 0, 0xAA, 0xBB]);
assert_eq!(decode(&g, &t, &r).unwrap(), data.to_vec());
}
/// Donnée PLUS COURTE que la prédiction : la longueur du résidu fait foi,
/// roundtrip exact sans ambiguïté.
#[test]
fn donnee_plus_courte_que_la_prediction() {
let g = ConstGenerator::new(0x11);
let data = [0x11, 0x22]; // prédiction : 8 octets
let (t, r) = encode(&g, &data, ConstGenerator::coords(8));
assert_eq!(r.len(), 2);
assert_eq!(decode(&g, &t, &r).unwrap(), data.to_vec());
}
/// Donnée vide + prédiction vide : résidu vide, roundtrip exact.
#[test]
fn donnee_vide() {
let g = IdentityGenerator;
let (t, r) = encode(&g, b"", Vec::new());
assert!(r.is_empty());
assert_eq!(decode(&g, &t, &r).unwrap(), Vec::<u8>::new());
}
/// Stabilité des identités : même description ⇒ même hash (partout,
/// toujours) ; descriptions différentes ⇒ hash différents.
#[test]
fn hash_generateurs_stables_et_distincts() {
assert_eq!(ConstGenerator::new(5).hash(), ConstGenerator::new(5).hash());
assert_ne!(ConstGenerator::new(5).hash(), ConstGenerator::new(6).hash());
assert_ne!(
PatternGenerator::new(vec![5]).hash(),
ConstGenerator::new(5).hash(),
"familles différentes = identités différentes, même paramètre"
);
assert_ne!(IdentityGenerator.hash(), ConstGenerator::new(0).hash());
}
/// Coordonnées malformées : un générateur ne panique jamais, il prédit
/// « rien » et le résidu porte tout.
#[test]
fn coordonnees_malformees_predisent_rien() {
let g = ConstGenerator::new(9);
let data = b"survit";
let (t, r) = encode(&g, data, vec![1, 2, 3]); // ≠ 8 octets
assert_eq!(r, data.to_vec());
assert_eq!(decode(&g, &t, &r).unwrap(), data.to_vec());
let p = PatternGenerator::new(vec![1, 2]);
let (t2, r2) = encode(&p, data, vec![0; 5]); // ≠ 16 octets
assert_eq!(r2, data.to_vec());
assert_eq!(decode(&p, &t2, &r2).unwrap(), data.to_vec());
}
/// Le triplet ne contient aucun octet de contenu : deux données
/// différentes de même longueur sous le même générateur ne diffèrent que
/// par `res_hash` — et le `res_hash` ne révèle pas la donnée.
#[test]
fn triplet_est_pure_adresse() {
let g = ConstGenerator::new(0);
let (a, _) = encode(&g, b"secret-1", ConstGenerator::coords(8));
let (b, _) = encode(&g, b"secret-2", ConstGenerator::coords(8));
assert_eq!(a.gen_hash, b.gen_hash);
assert_eq!(a.coords, b.coords);
assert_ne!(a.res_hash, b.res_hash);
}
}