tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definitions

⎇ main crates/takana-cli/src/fat_ro.rs Rust · 718 lines · 27.9 KB History Who touched what raw

boot entry backup/restore: el arranque como estado reproducible (ADR 0018 §5) bd1fa8bf8 · Sergio ·

1//! Lector FAT de **sólo lectura**, para mirar una ESP sin montarla (ADR 0018 §4).
2//!
3//! ── POR QUÉ NO SE MONTA Y YA ────────────────────────────────────────────────────────────────────
4//! El §4 pide que el instalador reporte el terreno **antes de tocar nada**: qué hay en la ESP,
5//! cuánto espacio queda y con quién se comparte. Montar para averiguarlo tiene tres problemas: pide
6//! privilegios, deja un efecto secundario justo cuando prometimos no tocar nada, y falla feo si el
7//! sistema operativo del vecino dejó la FAT sucia por su hibernación (el «Fast Startup» de Windows).
8//!
9//! Leer la estructura es más barato que todo eso: el BPB da la geometría, la FAT da los clusters
10//! libres y el directorio da los vendors. Nada de esto escribe un byte.
11//!
12//! ⚠ Sólo entiende lo que hace falta y lo dice cuando no entiende algo: FAT12/16/32 para la
13//! geometría, nombres 8.3 y LFN para el listado. No hay escritura ni la va a haber — para eso está
14//! mtools, que ya se usa en los scripts de imagen.
15
16use std::io::{self, Read, Seek, SeekFrom};
17use std::path::Path;
18
19/// Geometría y ocupación de un volumen FAT.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct FatInfo {
22    pub bytes_por_sector: u32,
23    pub sectores_por_cluster: u32,
24    pub total_sectores: u64,
25    pub clusters_totales: u64,
26    pub clusters_libres: u64,
27    /// 12, 16 o 32.
28    pub tipo: u8,
29    pub etiqueta: String,
30}
31
32impl FatInfo {
33    pub fn bytes_totales(&self) -> u64 {
34        self.total_sectores * self.bytes_por_sector as u64
35    }
36    pub fn bytes_libres(&self) -> u64 {
37        self.clusters_libres * self.sectores_por_cluster as u64 * self.bytes_por_sector as u64
38    }
39    pub fn bytes_usados(&self) -> u64 {
40        let cl_usados = self.clusters_totales.saturating_sub(self.clusters_libres);
41        cl_usados * self.sectores_por_cluster as u64 * self.bytes_por_sector as u64
42    }
43}
44
45/// Una entrada de directorio, ya resuelta a nombre legible.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Entrada {
48    pub nombre: String,
49    pub es_dir: bool,
50    pub tam: u32,
51}
52
53fn le16(b: &[u8], o: usize) -> u32 {
54    u16::from_le_bytes([b[o], b[o + 1]]) as u32
55}
56fn le32(b: &[u8], o: usize) -> u32 {
57    u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
58}
59
60/// Abre el volumen FAT que empieza en `offset` bytes dentro de `dev`.
61pub struct Fat {
62    f: std::fs::File,
63    base: u64,
64    pub info: FatInfo,
65    fat_inicio: u64,
66    datos_inicio: u64,
67    root_cluster: u32,
68    /// FAT12/16: el root es un área fija, no una cadena de clusters.
69    root_fijo: Option<(u64, u32)>,
70}
71
72impl Fat {
73    pub fn abrir(dev: &Path, offset: u64) -> io::Result<Fat> {
74        let mut f = std::fs::File::open(dev)?;
75        let mut bpb = [0u8; 512];
76        f.seek(SeekFrom::Start(offset))?;
77        f.read_exact(&mut bpb)?;
78
79        if bpb[510] != 0x55 || bpb[511] != 0xAA {
80            return Err(io::Error::new(
81                io::ErrorKind::InvalidData,
82                "no parece un volumen FAT: falta la firma 0x55AA",
83            ));
84        }
85        let bps = le16(&bpb, 0x0B);
86        let spc = bpb[0x0D] as u32;
87        // Un BPB con ceros acá no es «FAT vacía»: es que no estamos mirando un BPB.
88        if !(512..=4096).contains(&bps) || !bps.is_power_of_two() || spc == 0 || !spc.is_power_of_two()
89        {
90            return Err(io::Error::new(
91                io::ErrorKind::InvalidData,
92                format!("BPB imposible: {bps} B/sector, {spc} sectores/cluster"),
93            ));
94        }
95        let reservados = le16(&bpb, 0x0E);
96        let n_fats = bpb[0x10] as u32;
97        let root_entries = le16(&bpb, 0x11);
98        let tot16 = le16(&bpb, 0x13) as u64;
99        let fat16 = le16(&bpb, 0x16) as u64;
100        let tot32 = le32(&bpb, 0x20) as u64;
101        let fat32 = le32(&bpb, 0x24) as u64;
102
103        let total_sectores = if tot16 != 0 { tot16 } else { tot32 };
104        let fat_sectores = if fat16 != 0 { fat16 } else { fat32 };
105        if n_fats == 0 || fat_sectores == 0 || total_sectores == 0 {
106            return Err(io::Error::new(io::ErrorKind::InvalidData, "BPB sin FAT ni sectores"));
107        }
108
109        // El área del root es fija en FAT12/16 y una cadena de clusters en FAT32.
110        let root_sectores = (root_entries * 32).div_ceil(bps) as u64;
111        let primer_dato = reservados as u64 + n_fats as u64 * fat_sectores + root_sectores;
112        let clusters_totales = (total_sectores - primer_dato) / spc as u64;
113
114        // El tipo se decide POR EL NÚMERO DE CLUSTERS, no por la cadena de texto del BPB: esa es
115        // informativa y hay formateadores que mienten en ella.
116        let tipo = if clusters_totales < 4085 {
117            12
118        } else if clusters_totales < 65525 {
119            16
120        } else {
121            32
122        };
123
124        let etiqueta = {
125            let off = if tipo == 32 { 0x47 } else { 0x2B };
126            String::from_utf8_lossy(&bpb[off..off + 11]).trim().to_string()
127        };
128
129        let fat_inicio = offset + reservados as u64 * bps as u64;
130        let datos_inicio = offset + primer_dato * bps as u64;
131        let root_fijo = if tipo == 32 {
132            None
133        } else {
134            Some((
135                offset + (reservados as u64 + n_fats as u64 * fat_sectores) * bps as u64,
136                root_entries,
137            ))
138        };
139
140        let mut fat = Fat {
141            f,
142            base: offset,
143            info: FatInfo {
144                bytes_por_sector: bps,
145                sectores_por_cluster: spc,
146                total_sectores,
147                clusters_totales,
148                clusters_libres: 0,
149                tipo,
150                etiqueta,
151            },
152            fat_inicio,
153            datos_inicio,
154            root_cluster: if tipo == 32 { le32(&bpb, 0x2C) } else { 0 },
155            root_fijo,
156        };
157        fat.info.clusters_libres = fat.contar_libres()?;
158        Ok(fat)
159    }
160
161    /// Cuenta clusters libres recorriendo la FAT. **No se usa el `FSInfo`** de FAT32 a propósito:
162    /// ese campo es una pista que el sistema operativo que desmontó mal deja desactualizada, y acá
163    /// el número se usa para decidir si una instalación entra — un número optimista de más haría
164    /// fallar la instalación a mitad, que es justo lo que el §4 existe para evitar.
165    fn contar_libres(&mut self) -> io::Result<u64> {
166        let n = self.info.clusters_totales;
167        let mut libres = 0u64;
168        match self.info.tipo {
169            32 => {
170                let mut buf = vec![0u8; 4096];
171                let mut leidos = 0u64;
172                self.f.seek(SeekFrom::Start(self.fat_inicio))?;
173                // Los clusters 0 y 1 son reservados: la cuenta útil empieza en 2.
174                while leidos < n + 2 {
175                    let k = self.f.read(&mut buf)?;
176                    if k == 0 {
177                        break;
178                    }
179                    for c in buf[..k].chunks_exact(4) {
180                        if leidos >= 2 && leidos < n + 2 && le32(c, 0) & 0x0FFF_FFFF == 0 {
181                            libres += 1;
182                        }
183                        leidos += 1;
184                    }
185                }
186            }
187            16 => {
188                let mut buf = vec![0u8; 4096];
189                let mut leidos = 0u64;
190                self.f.seek(SeekFrom::Start(self.fat_inicio))?;
191                while leidos < n + 2 {
192                    let k = self.f.read(&mut buf)?;
193                    if k == 0 {
194                        break;
195                    }
196                    for c in buf[..k].chunks_exact(2) {
197                        if leidos >= 2 && leidos < n + 2 && le16(c, 0) == 0 {
198                            libres += 1;
199                        }
200                        leidos += 1;
201                    }
202                }
203            }
204            _ => {
205                // FAT12 son entradas de 12 bits a caballo entre bytes. Una ESP nunca es FAT12, así
206                // que en vez de implementarlo a medias se declara desconocido: un 0 aquí se leería
207                // como «llena» y eso es una mentira con consecuencias.
208                return Ok(0);
209            }
210        }
211        Ok(libres)
212    }
213
214    fn siguiente_cluster(&mut self, c: u32) -> io::Result<Option<u32>> {
215        let (off, val) = match self.info.tipo {
216            32 => {
217                let mut b = [0u8; 4];
218                self.f.seek(SeekFrom::Start(self.fat_inicio + c as u64 * 4))?;
219                self.f.read_exact(&mut b)?;
220                (0, le32(&b, 0) & 0x0FFF_FFFF)
221            }
222            16 => {
223                let mut b = [0u8; 2];
224                self.f.seek(SeekFrom::Start(self.fat_inicio + c as u64 * 2))?;
225                self.f.read_exact(&mut b)?;
226                (0, le16(&b, 0))
227            }
228            _ => return Ok(None),
229        };
230        let _ = off;
231        let fin = if self.info.tipo == 32 { 0x0FFF_FFF8 } else { 0xFFF8 };
232        if val >= fin || val < 2 {
233            Ok(None)
234        } else {
235            Ok(Some(val))
236        }
237    }
238
239    fn leer_cluster(&mut self, c: u32) -> io::Result<Vec<u8>> {
240        let tam = self.info.sectores_por_cluster as usize * self.info.bytes_por_sector as usize;
241        let mut v = vec![0u8; tam];
242        let off = self.datos_inicio + (c as u64 - 2) * tam as u64;
243        self.f.seek(SeekFrom::Start(off))?;
244        self.f.read_exact(&mut v)?;
245        Ok(v)
246    }
247
248    /// Entradas del directorio raíz.
249    pub fn raiz(&mut self) -> io::Result<Vec<Entrada>> {
250        if let Some((off, n)) = self.root_fijo {
251            let mut v = vec![0u8; n as usize * 32];
252            self.f.seek(SeekFrom::Start(off))?;
253            self.f.read_exact(&mut v)?;
254            return Ok(parse_dir(&v));
255        }
256        self.leer_cadena(self.root_cluster).map(|b| parse_dir(&b))
257    }
258
259    /// Entradas de un subdirectorio del raíz (un solo nivel, que es lo que el §4 necesita: ver los
260    /// *vendors* que cuelgan de `\EFI`).
261    pub fn subdir(&mut self, nombre: &str) -> io::Result<Option<Vec<Entrada>>> {
262        let dir = if let Some((off, n)) = self.root_fijo {
263            let mut v = vec![0u8; n as usize * 32];
264            self.f.seek(SeekFrom::Start(off))?;
265            self.f.read_exact(&mut v)?;
266            v
267        } else {
268            self.leer_cadena(self.root_cluster)?
269        };
270        let Some(c) = buscar_cluster(&dir, nombre) else { return Ok(None) };
271        if c < 2 {
272            return Ok(None);
273        }
274        Ok(Some(parse_dir(&self.leer_cadena(c)?)))
275    }
276
277    /// Lee un fichero por ruta absoluta dentro del volumen (`/EFI/takana/takanax64.efi`).
278    ///
279    /// Trunca al **tamaño declarado** en la entrada de directorio, no al final del último cluster:
280    /// un fichero de 15 MiB en clusters de 1 KiB termina con relleno, y hashear el relleno daría un
281    /// hash distinto al del mismo fichero en disco. Es el tipo de diferencia que sólo se descubre
282    /// cuando dos respaldos del mismo arranque no coinciden.
283    pub fn leer_archivo(&mut self, ruta: &str) -> io::Result<Option<Vec<u8>>> {
284        let partes: Vec<&str> = ruta.split('/').filter(|x| !x.is_empty()).collect();
285        let Some((archivo, dirs)) = partes.split_last() else { return Ok(None) };
286
287        let mut dir = if let Some((off, n)) = self.root_fijo {
288            let mut v = vec![0u8; n as usize * 32];
289            self.f.seek(SeekFrom::Start(off))?;
290            self.f.read_exact(&mut v)?;
291            v
292        } else {
293            self.leer_cadena(self.root_cluster)?
294        };
295        for d in dirs {
296            let Some(c) = buscar_cluster(&dir, d) else { return Ok(None) };
297            if c < 2 {
298                return Ok(None);
299            }
300            dir = self.leer_cadena(c)?;
301        }
302        let Some((cluster, tam)) = buscar_entrada(&dir, archivo) else { return Ok(None) };
303        if cluster < 2 || tam == 0 {
304            return Ok(Some(Vec::new()));
305        }
306        let mut b = self.leer_cadena(cluster)?;
307        b.truncate(tam as usize);
308        Ok(Some(b))
309    }
310
311    /// Todos los ficheros del volumen hasta `prof` niveles, como rutas absolutas. Acotado a
312    /// propósito: una ESP es plana por convención y un recorrido sin tope se cuelga con un
313    /// directorio corrupto que se apunta a sí mismo.
314    pub fn listar_recursivo(&mut self, prof: u32) -> io::Result<Vec<(String, u32)>> {
315        let mut out = Vec::new();
316        let raiz = self.raiz()?;
317        self.recorrer(&raiz, "", prof, &mut out)?;
318        out.sort();
319        Ok(out)
320    }
321
322    fn recorrer(
323        &mut self,
324        entradas: &[Entrada],
325        prefijo: &str,
326        prof: u32,
327        out: &mut Vec<(String, u32)>,
328    ) -> io::Result<()> {
329        for e in entradas {
330            let ruta = format!("{prefijo}/{}", e.nombre);
331            if e.es_dir {
332                if prof == 0 {
333                    continue;
334                }
335                // Releer el directorio por su ruta, que es lo que `subdir` no da más allá del raíz.
336                let hijos = self.listar_ruta(&ruta)?;
337                self.recorrer(&hijos, &ruta, prof - 1, out)?;
338            } else {
339                out.push((ruta, e.tam));
340            }
341        }
342        Ok(())
343    }
344
345    /// Entradas del directorio en `ruta` (absoluta dentro del volumen).
346    pub fn listar_ruta(&mut self, ruta: &str) -> io::Result<Vec<Entrada>> {
347        let mut dir = if let Some((off, n)) = self.root_fijo {
348            let mut v = vec![0u8; n as usize * 32];
349            self.f.seek(SeekFrom::Start(off))?;
350            self.f.read_exact(&mut v)?;
351            v
352        } else {
353            self.leer_cadena(self.root_cluster)?
354        };
355        for d in ruta.split('/').filter(|x| !x.is_empty()) {
356            let Some(c) = buscar_cluster(&dir, d) else { return Ok(Vec::new()) };
357            if c < 2 {
358                return Ok(Vec::new());
359            }
360            dir = self.leer_cadena(c)?;
361        }
362        Ok(parse_dir(&dir))
363    }
364
365    fn leer_cadena(&mut self, inicio: u32) -> io::Result<Vec<u8>> {
366        let mut out = Vec::new();
367        let mut c = inicio;
368        // Tope duro: una cadena circular por corrupción colgaría el instalador, y colgar es peor
369        // que reportar de menos.
370        for _ in 0..65536 {
371            out.extend(self.leer_cluster(c)?);
372            match self.siguiente_cluster(c)? {
373                Some(n) => c = n,
374                None => break,
375            }
376        }
377        Ok(out)
378    }
379
380    pub fn base(&self) -> u64 {
381        self.base
382    }
383}
384
385fn nombre_83(e: &[u8]) -> String {
386    let base = String::from_utf8_lossy(&e[0..8]).trim_end().to_string();
387    let ext = String::from_utf8_lossy(&e[8..11]).trim_end().to_string();
388    if ext.is_empty() {
389        base
390    } else {
391        format!("{base}.{ext}")
392    }
393}
394
395/// Decodifica un área de entradas de directorio. Junta los LFN (nombres largos) cuando están, que
396/// es lo normal en una ESP escrita por Windows: sin esto `Microsoft` se vería como `MICROS~1`.
397fn parse_dir(b: &[u8]) -> Vec<Entrada> {
398    let mut out = Vec::new();
399    let mut lfn: Vec<(u8, Vec<u16>)> = Vec::new();
400    for e in b.chunks_exact(32) {
401        match e[0] {
402            0x00 => break,      // fin del directorio
403            0xE5 => {
404                lfn.clear();
405                continue;
406            } // borrada
407            _ => {}
408        }
409        let attr = e[0x0B];
410        if attr & 0x0F == 0x0F {
411            // Entrada LFN: 13 unidades UTF-16 repartidas en tres tramos.
412            let mut u = Vec::with_capacity(13);
413            for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] {
414                for i in 0..n {
415                    u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]]));
416                }
417            }
418            lfn.push((e[0] & 0x3F, u));
419            continue;
420        }
421        if attr & 0x08 != 0 {
422            lfn.clear();
423            continue; // etiqueta de volumen
424        }
425        let nombre = if lfn.is_empty() {
426            nombre_83(e)
427        } else {
428            lfn.sort_by_key(|(n, _)| *n);
429            let mut u: Vec<u16> = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect();
430            if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) {
431                u.truncate(p);
432            }
433            String::from_utf16_lossy(&u)
434        };
435        lfn.clear();
436        let nombre = nombre.trim().to_string();
437        if nombre == "." || nombre == ".." || nombre.is_empty() {
438            continue;
439        }
440        out.push(Entrada { nombre, es_dir: attr & 0x10 != 0, tam: le32(e, 0x1C) });
441    }
442    out
443}
444
445/// Como `buscar_cluster` pero devuelve `(cluster, tamaño)`: para un fichero el tamaño es
446/// imprescindible, porque la cadena de clusters siempre da de más.
447fn buscar_entrada(dir: &[u8], nombre: &str) -> Option<(u32, u32)> {
448    let mut lfn: Vec<(u8, Vec<u16>)> = Vec::new();
449    for e in dir.chunks_exact(32) {
450        if e[0] == 0x00 {
451            break;
452        }
453        if e[0] == 0xE5 {
454            lfn.clear();
455            continue;
456        }
457        let attr = e[0x0B];
458        if attr & 0x0F == 0x0F {
459            let mut u = Vec::with_capacity(13);
460            for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] {
461                for i in 0..n {
462                    u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]]));
463                }
464            }
465            lfn.push((e[0] & 0x3F, u));
466            continue;
467        }
468        let corto = nombre_83(e);
469        let largo = if lfn.is_empty() {
470            String::new()
471        } else {
472            lfn.sort_by_key(|(n, _)| *n);
473            let mut u: Vec<u16> = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect();
474            if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) {
475                u.truncate(p);
476            }
477            String::from_utf16_lossy(&u)
478        };
479        lfn.clear();
480        if corto.eq_ignore_ascii_case(nombre) || largo.eq_ignore_ascii_case(nombre) {
481            return Some(((le16(e, 0x14) << 16) | le16(e, 0x1A), le32(e, 0x1C)));
482        }
483    }
484    None
485}
486
487fn buscar_cluster(dir: &[u8], nombre: &str) -> Option<u32> {
488    let mut lfn: Vec<(u8, Vec<u16>)> = Vec::new();
489    for e in dir.chunks_exact(32) {
490        if e[0] == 0x00 {
491            break;
492        }
493        if e[0] == 0xE5 {
494            lfn.clear();
495            continue;
496        }
497        let attr = e[0x0B];
498        if attr & 0x0F == 0x0F {
499            let mut u = Vec::with_capacity(13);
500            for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] {
501                for i in 0..n {
502                    u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]]));
503                }
504            }
505            lfn.push((e[0] & 0x3F, u));
506            continue;
507        }
508        let corto = nombre_83(e);
509        let largo = if lfn.is_empty() {
510            String::new()
511        } else {
512            lfn.sort_by_key(|(n, _)| *n);
513            let mut u: Vec<u16> = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect();
514            if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) {
515                u.truncate(p);
516            }
517            String::from_utf16_lossy(&u)
518        };
519        lfn.clear();
520        if corto.eq_ignore_ascii_case(nombre) || largo.eq_ignore_ascii_case(nombre) {
521            return Some((le16(e, 0x14) << 16) | le16(e, 0x1A));
522        }
523    }
524    None
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use std::path::PathBuf;
531
532    /// BPB fabricado a mano: prueba la geometría y, sobre todo, que un BPB imposible se RECHACE.
533    #[test]
534    fn rechaza_un_bpb_imposible_en_vez_de_devolver_ceros() {
535        let tmp = std::env::temp_dir().join(format!("takana-fat-neg-{}", std::process::id()));
536        std::fs::create_dir_all(&tmp).unwrap();
537        // Todo ceros salvo la firma: bytes/sector = 0 ⇒ no es un BPB.
538        let mut d = vec![0u8; 512 * 64];
539        d[510] = 0x55;
540        d[511] = 0xAA;
541        let f = tmp.join("falso.img");
542        std::fs::write(&f, &d).unwrap();
543        let e = match Fat::abrir(&f, 0) {
544            Err(e) => e,
545            Ok(_) => panic!("un BPB de ceros NO puede abrirse como FAT"),
546        };
547        assert!(
548            e.to_string().contains("BPB imposible"),
549            "un BPB de ceros tiene que fallar, no leerse como «FAT llena»: {e}"
550        );
551        // Y sin la firma tampoco.
552        std::fs::write(&f, vec![0u8; 512 * 64]).unwrap();
553        assert!(Fat::abrir(&f, 0).is_err(), "sin 0x55AA no hay volumen");
554        std::fs::remove_dir_all(&tmp).ok();
555    }
556
557    /// Fabrica una entrada LFN + su 8.3, como las escribe Windows.
558    fn entradas_lfn(largo: &str, corto: &[u8; 11], dir: bool) -> Vec<u8> {
559        let u: Vec<u16> = largo.encode_utf16().collect();
560        let mut out = Vec::new();
561        let trozos: Vec<&[u16]> = u.chunks(13).collect();
562        // Los LFN se escriben AL REVÉS, con el último marcado 0x40.
563        for (i, t) in trozos.iter().enumerate().rev() {
564            let mut e = [0u8; 32];
565            e[0] = (i as u8 + 1) | if i == trozos.len() - 1 { 0x40 } else { 0 };
566            e[0x0B] = 0x0F;
567            let mut rel = Vec::from(*t);
568            while rel.len() < 13 {
569                rel.push(if rel.len() == t.len() { 0 } else { 0xFFFF });
570            }
571            let mut k = 0;
572            for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] {
573                for j in 0..n {
574                    e[o + j * 2..o + j * 2 + 2].copy_from_slice(&rel[k].to_le_bytes());
575                    k += 1;
576                }
577            }
578            out.extend_from_slice(&e);
579        }
580        let mut e = [0u8; 32];
581        e[0..11].copy_from_slice(corto);
582        e[0x0B] = if dir { 0x10 } else { 0x20 };
583        out.extend_from_slice(&e);
584        out
585    }
586
587    /// Fabrica una FAT16 mínima y válida con UN fichero de contenido conocido, sin depender de
588    /// mtools: así el test corre en cualquier entorno y no se saltea en silencio.
589    fn fat16_con_fichero(tmp: &Path, nombre_83: &[u8; 11], datos: &[u8]) -> PathBuf {
590        let bps = 512usize;
591        let reservados = 1usize;
592        let n_fats = 1usize;
593        let fat_sectores = 32usize;
594        let root_entries = 16usize;
595        let total = 8192usize; // ⇒ 8158 clusters: franja FAT16
596        let mut d = vec![0u8; bps * total];
597
598        d[0x0B..0x0D].copy_from_slice(&(bps as u16).to_le_bytes());
599        d[0x0D] = 1; // sectores por cluster
600        d[0x0E..0x10].copy_from_slice(&(reservados as u16).to_le_bytes());
601        d[0x10] = n_fats as u8;
602        d[0x11..0x13].copy_from_slice(&(root_entries as u16).to_le_bytes());
603        d[0x13..0x15].copy_from_slice(&(total as u16).to_le_bytes());
604        d[0x16..0x18].copy_from_slice(&(fat_sectores as u16).to_le_bytes());
605        d[0x2B..0x36].copy_from_slice(b"PRUEBA     ");
606        d[510] = 0x55;
607        d[511] = 0xAA;
608
609        // FAT: clusters 0 y 1 reservados; el 2 es el fin de la cadena de nuestro fichero.
610        let fat = reservados * bps;
611        d[fat..fat + 2].copy_from_slice(&0xFFF8u16.to_le_bytes());
612        d[fat + 2..fat + 4].copy_from_slice(&0xFFFFu16.to_le_bytes());
613        d[fat + 4..fat + 6].copy_from_slice(&0xFFFFu16.to_le_bytes());
614
615        // Directorio raíz: un fichero en el cluster 2.
616        let root = (reservados + n_fats * fat_sectores) * bps;
617        d[root..root + 11].copy_from_slice(nombre_83);
618        d[root + 0x0B] = 0x20; // archivo
619        d[root + 0x1A..root + 0x1C].copy_from_slice(&2u16.to_le_bytes());
620        d[root + 0x1C..root + 0x20].copy_from_slice(&(datos.len() as u32).to_le_bytes());
621
622        // Datos: el cluster 2 es el primer sector de datos.
623        let primer_dato = (reservados + n_fats * fat_sectores + root_entries * 32 / bps) * bps;
624        d[primer_dato..primer_dato + datos.len()].copy_from_slice(datos);
625
626        let f = tmp.join("fat16.img");
627        std::fs::write(&f, &d).unwrap();
628        f
629    }
630
631    #[test]
632    fn lee_un_fichero_entero_y_lo_trunca_al_tamano_declarado() {
633        let tmp = std::env::temp_dir().join(format!("takana-fat-file-{}", std::process::id()));
634        std::fs::create_dir_all(&tmp).unwrap();
635        // 300 bytes en un cluster de 512: el resto del cluster es RELLENO. Si no se trunca al
636        // tamaño declarado, el hash del respaldo del ADR 0018 §5 no coincidiría nunca con el del
637        // mismo fichero en disco — y el síntoma sería «dos respaldos del mismo arranque difieren».
638        let datos: Vec<u8> = (0..300u32).map(|i| (i % 251) as u8).collect();
639        let img = fat16_con_fichero(&tmp, b"KERNEL  EFI", &datos);
640        let mut fat = Fat::abrir(&img, 0).unwrap();
641        assert_eq!(fat.info.tipo, 16);
642
643        let leido = fat.leer_archivo("/KERNEL.EFI").unwrap().expect("el fichero está");
644        assert_eq!(leido.len(), 300, "truncado al tamaño declarado, no al del cluster");
645        assert_eq!(leido, datos, "y los bytes son exactamente los que se escribieron");
646
647        // Un fichero que no está devuelve None, no un vector vacío que se confundiría con «vacío».
648        assert!(fat.leer_archivo("/NO-ESTA.EFI").unwrap().is_none());
649        assert!(fat.leer_archivo("/SUB/NO-ESTA.EFI").unwrap().is_none());
650
651        let l = fat.listar_recursivo(3).unwrap();
652        assert_eq!(l, vec![("/KERNEL.EFI".to_string(), 300u32)]);
653        std::fs::remove_dir_all(&tmp).ok();
654    }
655
656    #[test]
657    fn el_nombre_largo_gana_al_8_3() {
658        // Sin juntar los LFN, «Microsoft» se vería como «MICROS~1» y la advertencia de vecino
659        // Windows —que es la razón de ser del §4— no dispararía nunca.
660        let mut d = entradas_lfn("Microsoft", b"MICROS~1   ", true);
661        d.extend(entradas_lfn("takana", b"TAKANA     ", true));
662        d.extend_from_slice(&[0u8; 32]); // fin de directorio
663        let v = parse_dir(&d);
664        assert_eq!(v.len(), 2, "dos directorios: {v:?}");
665        assert_eq!(v[0].nombre, "Microsoft", "el LFN manda sobre el 8.3");
666        assert!(v[0].es_dir);
667        assert_eq!(v[1].nombre, "takana");
668    }
669
670    #[test]
671    fn sin_lfn_se_usa_el_8_3_y_se_saltean_las_borradas() {
672        let mut d = Vec::new();
673        let mut e = [0u8; 32];
674        e[0..11].copy_from_slice(b"BOOT       ");
675        e[0x0B] = 0x10;
676        d.extend_from_slice(&e);
677        let mut borrada = [0u8; 32];
678        borrada[0] = 0xE5; // borrada: no debe aparecer
679        borrada[1..11].copy_from_slice(b"ORRADA    ");
680        d.extend_from_slice(&borrada);
681        let mut vol = [0u8; 32];
682        vol[0..11].copy_from_slice(b"ESP        ");
683        vol[0x0B] = 0x08; // etiqueta de volumen: tampoco es una entrada
684        d.extend_from_slice(&vol);
685        d.extend_from_slice(&[0u8; 32]);
686        let v = parse_dir(&d);
687        assert_eq!(v.len(), 1, "sólo BOOT: {v:?}");
688        assert_eq!(v[0].nombre, "BOOT");
689    }
690
691    #[test]
692    fn el_tipo_sale_del_numero_de_clusters_y_no_del_texto_del_bpb() {
693        // Un volumen chico es FAT16 aunque el BPB diga "FAT32   ": el texto es informativo y hay
694        // formateadores que mienten. La cuenta de clusters es la que manda (spec de Microsoft).
695        let tmp = std::env::temp_dir().join(format!("takana-fat-tipo-{}", std::process::id()));
696        std::fs::create_dir_all(&tmp).unwrap();
697        // 8192 sectores ⇒ (8192 − 49) = 8143 clusters, que cae en la franja FAT16 (4085..65525).
698        // El primer intento de este test usaba 2048 sectores = 1999 clusters, que es FAT12 de
699        // verdad: el que estaba mal era el TEST, no el código. Queda anotado porque la franja se
700        // olvida y el error se ve idéntico a un bug del lector.
701        let mut d = vec![0u8; 512 * 8192];
702        d[0x0B..0x0D].copy_from_slice(&512u16.to_le_bytes());
703        d[0x0D] = 1;
704        d[0x0E..0x10].copy_from_slice(&1u16.to_le_bytes());
705        d[0x10] = 2;
706        d[0x11..0x13].copy_from_slice(&512u16.to_le_bytes());
707        d[0x13..0x15].copy_from_slice(&8192u16.to_le_bytes());
708        d[0x16..0x18].copy_from_slice(&8u16.to_le_bytes());
709        d[0x52..0x5A].copy_from_slice(b"FAT32   "); // la mentira
710        d[510] = 0x55;
711        d[511] = 0xAA;
712        let f = tmp.join("chico.img");
713        std::fs::write(&f, &d).unwrap();
714        let fat = Fat::abrir(&f, 0).unwrap();
715        assert_eq!(fat.info.tipo, 16, "≈2000 clusters es FAT16, diga lo que diga el texto");
716        std::fs::remove_dir_all(&tmp).ok();
717    }
718}