🦀 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:
11
bion-triplet/Cargo.toml
Normal file
11
bion-triplet/Cargo.toml
Normal 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
106
bion-triplet/README.md
Normal 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
542
bion-triplet/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user