tawasuyu

tawasuyu/mirada

Sovereign tiling Wayland compositor + window manager in Rust, on Llimphi.

Rust 50Markdown 30TOML 211282 Definitions

⎇ 3dc85ebdcdd5fda0a1be0db5f8fbce5859266f2e shared/format/src/grafo.rs Rust · 400 lines · 19.1 KB History Who touched what raw

feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro) 3dc85ebdc · Sergio ·

1use super::*;
2
3// =============================================================================
4//  Fase 33/43 :: el almacen semantico del cuaderno (modelo unificado)
5// -----------------------------------------------------------------------------
6//  Un CUADERNO de Wawa es un nodo del grafo cuyo payload `postcard` es un
7//  `Vec<CeldaWawa>`. Cada `CeldaWawa` empaqueta TODA la informacion de un
8//  eslabon del calculo en una sola estructura inmutable:
9//
10//    * `id_secuencial`   :: indice lineal en el cuaderno.
11//    * `fuente_hash`     :: hash del texto Forth o token `@<hash>` literal.
12//    * `binario_hash`    :: hash del modulo WASM materializado (None si
13//                           la compilacion fallo).
14//    * `ultimo_retorno`  :: el i32 que la sub-jaula efimera devolvio
15//                           (None si nunca se ejecuto).
16//    * `marca_error`     :: bandera atomica: hubo TRAP, OUT_OF_FUEL,
17//                           PAYLOAD_INVALIDO, o cualquier otra falla.
18//
19//  La fusion (Fase 43) elimina el enum heredado `TipoCeldaWawa` con sus
20//  tres variantes flat — el modelo estructurado es mas honesto con la
21//  semantica del cuaderno y converge bit-a-bit con la representacion
22//  del motor Linux del ecosistema Pluma (`pluma-notebook-core`), que
23//  re-exporta esta misma struct para hablar el mismo idioma en host y
24//  en el silicio.
25//
26//  Las aristas del nodo (los `hijos` que el almacen registra al insertar)
27//  son: el CUADERNO PREVIO cuando existe (arista ancestral, Fase 47),
28//  `fuente_hash` siempre, `binario_hash` cuando esta presente. El
29//  direccionamiento por contenido hace EXPLICITAS las dependencias y
30//  el cuaderno arrastra criptograficamente todo su tejido de causas y
31//  efectos. Con la Fase 47, cada cuaderno apunta a su predecesor por
32//  hash — el historial es una cadena recorrible por el Walker.
33//
34//  Postcard-amigable: campos primitivos + `Option<T>` + arrays alineados.
35//  La deserializacion del cuaderno no allocea fuera del `Vec` principal.
36// =============================================================================
37
38/// El eslabon canonico de un cuaderno (Fase 43). Reemplaza al enum
39/// `TipoCeldaWawa` de la Fase 33: en lugar de tres variantes flat que
40/// solo el orden del `Vec` ataba a una "celda", aqui CADA `CeldaWawa`
41/// es una celda completa con todos sus eslabones bundled. Bit-compatible
42/// con `pluma_notebook_core::CeldaWawa` (re-export en el motor Linux).
43#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
44pub struct CeldaWawa {
45    /// Indice lineal en el cuaderno — orden de presentacion. Empieza
46    /// en 0 y crece con cada celda exitosamente registrada.
47    pub id_secuencial: u32,
48    /// Hash del texto fuente: ASCII Forth tecleado por el humano, o
49    /// la cadena literal `@<64-hex>` para celdas macro-importadas
50    /// (Fase 36, Cross-App Bridge). Siempre presente — una celda sin
51    /// fuente es incoherente con el modelo.
52    pub fuente_hash: Hash,
53    /// Hash del modulo WASM materializado por `forth-emisor` (o
54    /// importado del grafo via `@<hash>`). `None` cuando la compilacion
55    /// fallo, la sintaxis Forth fue rechazada, o la vinculacion macro
56    /// no se logro — el binario no llego a inscribirse.
57    pub binario_hash: Option<Hash>,
58    /// El i32 que la sub-jaula efimera (Fase 32) devolvio en su ultima
59    /// ejecucion. `None` cuando la celda nunca corrio (sin binario, o
60    /// el despacho dinamico ni siquiera arranco). Un valor negativo
61    /// en `[-7, -1]` reservado en `CodigoError` codifica fallas
62    /// controladas; valores fuera de ese rango son resultados legitimos.
63    pub ultimo_retorno: Option<i32>,
64    /// Bandera atomica de error: `true` si CUALQUIER eslabon de la
65    /// cadena (compilacion, registro v2, ejecucion dinamica, anclaje
66    /// de cuaderno) devolvio fallo. El renderer la usa para teñir la
67    /// celda de amarillo palido sin enterrar el valor del retorno —
68    /// `marca_error && ultimo_retorno == Some(-7)` significa
69    /// "ejecutada, fallida con trap"; `marca_error && ultimo_retorno
70    /// == None` significa "ni siquiera corrio".
71    pub marca_error: bool,
72}
73
74/// Serializa una secuencia de celdas a `postcard` — la forma que el
75/// kernel inscribe como payload del nodo cuaderno. Centralizada aqui
76/// para que el kernel no tenga que declarar `postcard` directamente
77/// (ya lo hereda transitivamente via `format`).
78pub fn serializar_celdas(celdas: &[CeldaWawa]) -> Result<Vec<u8>, &'static str> {
79    postcard::to_allocvec(celdas).map_err(|_| "celdas :: serializacion fallida")
80}
81
82/// Reconstruye la secuencia de celdas desde el payload de un nodo cuaderno.
83/// Tolera bytes sobrantes — el relleno del registro vive despues del payload.
84pub fn deserializar_celdas(bytes: &[u8]) -> Result<Vec<CeldaWawa>, &'static str> {
85    postcard::take_from_bytes::<Vec<CeldaWawa>>(bytes)
86        .map(|(celdas, _)| celdas)
87        .map_err(|_| "celdas :: deserializacion fallida")
88}
89
90// =============================================================================
91//  (De)serializacion — la forma binaria que viaja al disco
92// =============================================================================
93
94impl Objeto {
95    /// Serializa el objeto a su forma binaria `postcard`.
96    pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
97        postcard::to_allocvec(self).map_err(|_| "objeto :: serializacion fallida")
98    }
99
100    /// Reconstruye un objeto desde su forma binaria. Tolera bytes sobrantes
101    /// tras el objeto —el relleno del registro—: solo consume su prefijo.
102    pub fn deserializar(bytes: &[u8]) -> Result<Objeto, &'static str> {
103        postcard::take_from_bytes::<Objeto>(bytes)
104            .map(|(objeto, _)| objeto)
105            .map_err(|_| "objeto :: deserializacion fallida")
106    }
107}
108
109// =============================================================================
110//  Fase 66 :: Árbol/Blob — el monorepo como grafo
111// -----------------------------------------------------------------------------
112//  El grafo direccionado por contenido ES el modelo de objetos de git. Esta
113//  capa lo hace explícito para que un árbol de directorios viva en el grafo:
114//
115//    * BLOB      :: el contenido de un archivo. Es un `Objeto { datos: bytes,
116//                   hijos: [] }` — sin estructura, solo bytes direccionados por
117//                   su hash. Archivos idénticos comparten un solo blob (dedup
118//                   por contenido, gratis).
119//    * ÁRBOL     :: el contenido de un directorio. Un `Objeto` cuyo `datos` es
120//                   un `Arbol` postcard (la lista de entradas: nombre + modo +
121//                   hash) y cuyos `hijos` son los hashes de esas entradas — así
122//                   el MARK del GC del kernel alcanza todo el subárbol siguiendo
123//                   `hijos`, SIN tener que entender el format `Arbol`.
124//
125//  Las entradas de un árbol van ORDENADAS por nombre: mismo contenido de
126//  directorio => mismo árbol serializado => mismo hash. Determinismo total, la
127//  base de la dedup y de la verificación. Un repositorio entero colapsa a UN
128//  hash raíz; dos commits que solo tocan un archivo comparten todo el resto del
129//  árbol (estructura compartida, como git).
130// =============================================================================
131
132/// Version del format de un `Arbol`.
133pub const VERSION_ARBOL: u32 = 1;
134
135/// Qué clase de objeto referencia una entrada de árbol. Espeja los modos de
136/// git (archivo / archivo+x / symlink / directorio). Variantes AÑADIDAS AL
137/// FINAL: los tags `postcard` se asignan por orden y mover una romperia árboles
138/// ya serializados.
139#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
140pub enum ModoEntrada {
141    /// El hash apunta a un archivo regular (blob plano o índice de trozos).
142    Archivo,
143    /// El hash apunta a otro ÁRBOL (subdirectorio).
144    Directorio,
145    /// Como `Archivo` pero con bit de ejecución (un script, un binario).
146    Ejecutable,
147    /// El hash apunta a un blob cuyo contenido es el DESTINO del enlace
148    /// simbólico (la ruta a la que apunta), en UTF-8.
149    Symlink,
150}
151
152impl ModoEntrada {
153    /// `true` si el modo referencia CONTENIDO de archivo (blob/índice): un
154    /// archivo regular o un ejecutable. `Symlink` y `Directorio` no.
155    pub fn es_archivo(&self) -> bool {
156        matches!(self, ModoEntrada::Archivo | ModoEntrada::Ejecutable)
157    }
158}
159
160/// Una entrada de un árbol: un nombre dentro del directorio + el modo + el hash
161/// del objeto que la realiza (un blob si `Archivo`, un árbol si `Directorio`).
162#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
163pub struct EntradaArbol {
164    /// Nombre del archivo/subdirectorio (sin separadores de ruta).
165    pub nombre: String,
166    /// Si el hash apunta a un blob o a un subárbol.
167    pub modo: ModoEntrada,
168    /// Hash del objeto (blob o árbol) que esta entrada referencia.
169    pub hash: Hash,
170}
171
172/// Un árbol: el contenido de un directorio, como lista ordenada de entradas.
173#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
174pub struct Arbol {
175    /// Version del format — debe ser [`VERSION_ARBOL`].
176    pub version: u32,
177    /// Entradas ORDENADAS por nombre (invariante que `objeto_arbol` impone).
178    pub entradas: Vec<EntradaArbol>,
179}
180
181impl Arbol {
182    /// Serializa el árbol a su forma `postcard` —la carga útil del objeto que
183    /// lo aloja—.
184    pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
185        postcard::to_allocvec(self).map_err(|_| "arbol :: serializacion fallida")
186    }
187
188    /// Reconstruye un árbol desde la carga útil de su objeto. Rechaza una
189    /// version desconocida en lugar de malinterpretarla.
190    pub fn deserializar(bytes: &[u8]) -> Result<Arbol, &'static str> {
191        let (arbol, _) =
192            postcard::take_from_bytes::<Arbol>(bytes).map_err(|_| "arbol :: deserializacion fallida")?;
193        if arbol.version != VERSION_ARBOL {
194            return Err("arbol :: version de format desconocida");
195        }
196        Ok(arbol)
197    }
198}
199
200/// Construye el objeto BLOB de un archivo: bytes crudos, sin hijos. El hash de
201/// este objeto (sobre su forma serializada) es la identidad del archivo en el
202/// grafo. Dos archivos con idéntico contenido producen el MISMO blob.
203pub fn objeto_blob(datos: Vec<u8>) -> Objeto {
204    Objeto {
205        datos,
206        hijos: Vec::new(),
207    }
208}
209
210/// Construye el objeto ÍNDICE de un archivo GRANDE partido en trozos: `datos`
211/// VACÍO, `hijos` = los hashes de los blobs-trozo EN ORDEN. La convención de
212/// lectura: una entrada de archivo (`Archivo`/`Ejecutable`) cuyo objeto tiene
213/// `hijos` no vacío es un índice, y el contenido del archivo es la
214/// concatenación de los `datos` de sus trozos; si `hijos` está vacío, el
215/// objeto ES el contenido (blob plano). Así un archivo de cualquier tamaño se
216/// referencia igual desde el árbol — el lector decide plano vs índice por la
217/// forma del objeto, sin un modo aparte. Un archivo vacío es un blob plano
218/// (`datos` vacío, `hijos` vacío), nunca un índice.
219pub fn objeto_blob_indice(hijos: Vec<Hash>) -> Objeto {
220    Objeto {
221        datos: Vec::new(),
222        hijos,
223    }
224}
225
226/// Construye el objeto ÁRBOL de un directorio a partir de sus entradas. ORDENA
227/// las entradas por nombre (determinismo: mismo directorio → mismo hash) y fija
228/// `hijos` con los hashes de las entradas, en el MISMO orden, para que el GC
229/// alcance el subárbol siguiendo `hijos` sin parsear el `Arbol`.
230pub fn objeto_arbol(mut entradas: Vec<EntradaArbol>) -> Result<Objeto, &'static str> {
231    entradas.sort_by(|a, b| a.nombre.cmp(&b.nombre));
232    let hijos: Vec<Hash> = entradas.iter().map(|e| e.hash).collect();
233    let arbol = Arbol {
234        version: VERSION_ARBOL,
235        entradas,
236    };
237    let datos = arbol.serializar()?;
238    Ok(Objeto { datos, hijos })
239}
240
241impl SuperBloque {
242    /// Serializa el superbloque a su forma binaria `postcard`.
243    pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
244        postcard::to_allocvec(self).map_err(|_| "superbloque :: serializacion fallida")
245    }
246
247    /// Reconstruye el superbloque desde el sector 0. Tolera el relleno a cero
248    /// que completa el sector: solo consume el prefijo serializado.
249    pub fn deserializar(bytes: &[u8]) -> Result<SuperBloque, &'static str> {
250        postcard::take_from_bytes::<SuperBloque>(bytes)
251            .map(|(sb, _)| sb)
252            .map_err(|_| "superbloque :: deserializacion fallida")
253    }
254}
255
256impl Manifiesto {
257    /// Serializa el manifiesto a su forma binaria `postcard` — la carga util
258    /// del objeto del grafo que lo aloja.
259    pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
260        postcard::to_allocvec(self).map_err(|_| "manifiesto :: serializacion fallida")
261    }
262
263    /// Reconstruye un manifiesto desde la carga util de su objeto. Rechaza un
264    /// format de version desconocida en lugar de malinterpretarlo.
265    pub fn deserializar(bytes: &[u8]) -> Result<Manifiesto, &'static str> {
266        let (manifiesto, _) = postcard::take_from_bytes::<Manifiesto>(bytes)
267            .map_err(|_| "manifiesto :: deserializacion fallida")?;
268        if manifiesto.version != VERSION_MANIFIESTO {
269            return Err("manifiesto :: version de format desconocida");
270        }
271        Ok(manifiesto)
272    }
273}
274
275impl Canal {
276    /// Serializa el canal a su forma binaria `postcard` — la carga util del
277    /// objeto del grafo que lo aloja. Rechaza por adelantado un nombre que
278    /// supere [`NOMBRE_CANAL_LIMITE`]: mejor un error de serializacion que un
279    /// canal grafico que no quepa en disco.
280    pub fn serializar(&self) -> Result<Vec<u8>, &'static str> {
281        if self.nombre.len() > NOMBRE_CANAL_LIMITE {
282            return Err("canal :: nombre demasiado largo");
283        }
284        postcard::to_allocvec(self).map_err(|_| "canal :: serializacion fallida")
285    }
286
287    /// Reconstruye un canal desde la carga util de su objeto. Rechaza version
288    /// desconocida y nombres que excedan [`NOMBRE_CANAL_LIMITE`] —un canal con
289    /// nombre extravagante se detecta al recibirlo, no al servirlo—.
290    pub fn deserializar(bytes: &[u8]) -> Result<Canal, &'static str> {
291        let (canal, _) = postcard::take_from_bytes::<Canal>(bytes)
292            .map_err(|_| "canal :: deserializacion fallida")?;
293        if canal.version != VERSION_CANAL {
294            return Err("canal :: version de format desconocida");
295        }
296        if canal.nombre.len() > NOMBRE_CANAL_LIMITE {
297            return Err("canal :: nombre excede el techo");
298        }
299        Ok(canal)
300    }
301
302    /// La recomendacion vigente del canal: la ultima `RaizFirmada` por
303    /// `timestamp`, o `None` si el canal aun no propuso ninguna. Quien quiera
304    /// "actualizar" sigue este hash; quien quiera rollback elige otra entrada
305    /// del historial.
306    pub fn vigente(&self) -> Option<&RaizFirmada> {
307        self.raices.last()
308    }
309}
310
311/// Compone el mensaje canonico que un autor firma para respaldar una raiz en
312/// un canal: la concatenacion `nombre || timestamp_le || raiz_manifiesto`.
313/// Es la unica verdad del payload firmable —quien firma y quien verifica han
314/// de componerlo por aqui, jamas a mano—. La canonizacion incluye el nombre
315/// del canal para que una firma valida en `dev` no se replique en `estable`.
316pub fn mensaje_a_firmar(nombre_canal: &str, timestamp: u64, raiz_manifiesto: &Hash) -> Vec<u8> {
317    let mut mensaje = Vec::with_capacity(nombre_canal.len() + 8 + raiz_manifiesto.len());
318    mensaje.extend_from_slice(nombre_canal.as_bytes());
319    mensaje.extend_from_slice(&timestamp.to_le_bytes());
320    mensaje.extend_from_slice(raiz_manifiesto);
321    mensaje
322}
323
324/// Compone el mensaje canonico que un autor firma para CONCEDER capacidad a un
325/// bytecode: `bytecode(32) || permisos_le(4)`. Es la unica verdad del payload
326/// firmable de una [`ConcesionCapacidad`] —firmante y verificador lo componen
327/// por aqui, jamas a mano—. Liga la firma al hash EXACTO del binario y al
328/// bitfield EXACTO: una concesion para el bytecode X no vale para Y, y subir un
329/// bit de permiso invalida la firma. Devuelve un arreglo de pila de 36 bytes:
330/// zero-alloc, apto para el camino Ring 0 del kernel.
331pub fn mensaje_capacidad(bytecode: &Hash, permisos: Permisos) -> [u8; 36] {
332    let mut m = [0u8; 36];
333    m[..32].copy_from_slice(bytecode);
334    m[32..].copy_from_slice(&permisos.to_le_bytes());
335    m
336}
337
338/// Dominio de separacion del mensaje de ROTACION de clave. Un byte canonico de
339/// rotacion jamas colisiona con uno de revocacion ni con un claim del grafo.
340pub const DOM_ROTACION_CLAVE: &[u8] = b"agora-key-rotation\x01";
341
342/// Dominio de separacion del mensaje de REVOCACION de clave.
343pub const DOM_REVOCACION_CLAVE: &[u8] = b"agora-revocation\x01";
344
345/// Compone el mensaje canonico de una ROTACION de clave (handoff voluntario
346/// vieja->nueva): `DOM || old(32) || new(32) || issued_at_le(8)`. Tamanos fijos,
347/// sin prefijos de largo; el dominio lo separa de otros records. Es la unica
348/// verdad del payload firmable de una rotacion — `agora-core::KeyRotation` lo
349/// compone por aqui y el kernel lo espeja sobre estos mismos bytes (ver
350/// `agora/SDD-rotacion-revocacion.md` §2.1).
351pub fn mensaje_rotacion_clave(old_key: &[u8; 32], new_key: &[u8; 32], issued_at: u64) -> Vec<u8> {
352    let mut out = Vec::with_capacity(DOM_ROTACION_CLAVE.len() + 72);
353    out.extend_from_slice(DOM_ROTACION_CLAVE);
354    out.extend_from_slice(old_key);
355    out.extend_from_slice(new_key);
356    out.extend_from_slice(&issued_at.to_le_bytes());
357    out
358}
359
360/// Compone el mensaje canonico de una REVOCACION de clave:
361/// `DOM || target(32) || [motivo] || issued_at_le(8) || tag || [expires_le(8)]`,
362/// donde `tag` es `0` si `expires_at` es `None` y `1` si es `Some` (para que
363/// `None` y `Some(0)` no colisionen). El `motivo` es el discriminante estable de
364/// `agora-core::RevReason` (0=Compromised, 1=Retired, 2=Superseded) — entra en la
365/// firma para que no se pueda "ascender" un retiro a compromiso sin re-firmar.
366/// Unica verdad del payload firmable de una revocacion: `agora-core::Revocation`
367/// lo compone por aqui y el kernel lo espeja en `claves::verificar_revocacion`
368/// (ver `agora/SDD-rotacion-revocacion.md` §2.2 y §4).
369pub fn mensaje_revocacion_clave(
370    target_key: &[u8; 32],
371    motivo: u8,
372    issued_at: u64,
373    expires_at: Option<u64>,
374) -> Vec<u8> {
375    let mut out = Vec::with_capacity(DOM_REVOCACION_CLAVE.len() + 50);
376    out.extend_from_slice(DOM_REVOCACION_CLAVE);
377    out.extend_from_slice(target_key);
378    out.push(motivo);
379    out.extend_from_slice(&issued_at.to_le_bytes());
380    match expires_at {
381        None => out.push(0),
382        Some(t) => {
383            out.push(1);
384            out.extend_from_slice(&t.to_le_bytes());
385        }
386    }
387    out
388}
389
390/// Permisos EFECTIVOS de una app: la INTERSECCION de lo que su `EntradaApp` del
391/// manifiesto DECLARA y lo que una [`ConcesionCapacidad`] valida CONCEDE para su
392/// bytecode. El manifiesto no puede escalar un binario mas alla de su concesion
393/// firmada, y una concesion generosa no enciende permisos que el manifiesto no
394/// pidio. Sin concesion valida, el llamante pasa `0` como `concedidos` y la app
395/// corre sin capacidades gateadas (la matriz pasiva siempre esta). Es la regla
396/// que el kernel aplica en el punto de carga —ver `SDD-capacidades.md`—.
397pub const fn permisos_efectivos(declarados: Permisos, concedidos: Permisos) -> Permisos {
398    declarados & concedidos
399}
400