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…
⎇ 2201fac08e77b430ca7544672c009c5c8622c119
crates/takana-cli/src/fat_ro.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
boot entry survey: el informe del terreno, leyendo la ESP SIN montarla (ADR 0018 §4)
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 fn leer_cadena(&mut self, inicio: u32) -> io::Result<Vec<u8>> {
278 let mut out = Vec::new();
279 let mut c = inicio;
280 // Tope duro: una cadena circular por corrupción colgaría el instalador, y colgar es peor
281 // que reportar de menos.
282 for _ in 0..65536 {
283 out.extend(self.leer_cluster(c)?);
284 match self.siguiente_cluster(c)? {
285 Some(n) => c = n,
286 None => break,
287 }
288 }
289 Ok(out)
290 }
291
292 pub fn base(&self) -> u64 {
293 self.base
294 }
295}
296
297fn nombre_83(e: &[u8]) -> String {
298 let base = String::from_utf8_lossy(&e[0..8]).trim_end().to_string();
299 let ext = String::from_utf8_lossy(&e[8..11]).trim_end().to_string();
300 if ext.is_empty() {
301 base
302 } else {
303 format!("{base}.{ext}")
304 }
305}
306
307/// Decodifica un área de entradas de directorio. Junta los LFN (nombres largos) cuando están, que
308/// es lo normal en una ESP escrita por Windows: sin esto `Microsoft` se vería como `MICROS~1`.
309fn parse_dir(b: &[u8]) -> Vec<Entrada> {
310 let mut out = Vec::new();
311 let mut lfn: Vec<(u8, Vec<u16>)> = Vec::new();
312 for e in b.chunks_exact(32) {
313 match e[0] {
314 0x00 => break, // fin del directorio
315 0xE5 => {
316 lfn.clear();
317 continue;
318 } // borrada
319 _ => {}
320 }
321 let attr = e[0x0B];
322 if attr & 0x0F == 0x0F {
323 // Entrada LFN: 13 unidades UTF-16 repartidas en tres tramos.
324 let mut u = Vec::with_capacity(13);
325 for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] {
326 for i in 0..n {
327 u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]]));
328 }
329 }
330 lfn.push((e[0] & 0x3F, u));
331 continue;
332 }
333 if attr & 0x08 != 0 {
334 lfn.clear();
335 continue; // etiqueta de volumen
336 }
337 let nombre = if lfn.is_empty() {
338 nombre_83(e)
339 } else {
340 lfn.sort_by_key(|(n, _)| *n);
341 let mut u: Vec<u16> = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect();
342 if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) {
343 u.truncate(p);
344 }
345 String::from_utf16_lossy(&u)
346 };
347 lfn.clear();
348 let nombre = nombre.trim().to_string();
349 if nombre == "." || nombre == ".." || nombre.is_empty() {
350 continue;
351 }
352 out.push(Entrada { nombre, es_dir: attr & 0x10 != 0, tam: le32(e, 0x1C) });
353 }
354 out
355}
356
357fn buscar_cluster(dir: &[u8], nombre: &str) -> Option<u32> {
358 let mut lfn: Vec<(u8, Vec<u16>)> = Vec::new();
359 for e in dir.chunks_exact(32) {
360 if e[0] == 0x00 {
361 break;
362 }
363 if e[0] == 0xE5 {
364 lfn.clear();
365 continue;
366 }
367 let attr = e[0x0B];
368 if attr & 0x0F == 0x0F {
369 let mut u = Vec::with_capacity(13);
370 for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] {
371 for i in 0..n {
372 u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]]));
373 }
374 }
375 lfn.push((e[0] & 0x3F, u));
376 continue;
377 }
378 let corto = nombre_83(e);
379 let largo = if lfn.is_empty() {
380 String::new()
381 } else {
382 lfn.sort_by_key(|(n, _)| *n);
383 let mut u: Vec<u16> = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect();
384 if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) {
385 u.truncate(p);
386 }
387 String::from_utf16_lossy(&u)
388 };
389 lfn.clear();
390 if corto.eq_ignore_ascii_case(nombre) || largo.eq_ignore_ascii_case(nombre) {
391 return Some((le16(e, 0x14) << 16) | le16(e, 0x1A));
392 }
393 }
394 None
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 /// BPB fabricado a mano: prueba la geometría y, sobre todo, que un BPB imposible se RECHACE.
402 #[test]
403 fn rechaza_un_bpb_imposible_en_vez_de_devolver_ceros() {
404 let tmp = std::env::temp_dir().join(format!("takana-fat-neg-{}", std::process::id()));
405 std::fs::create_dir_all(&tmp).unwrap();
406 // Todo ceros salvo la firma: bytes/sector = 0 ⇒ no es un BPB.
407 let mut d = vec![0u8; 512 * 64];
408 d[510] = 0x55;
409 d[511] = 0xAA;
410 let f = tmp.join("falso.img");
411 std::fs::write(&f, &d).unwrap();
412 let e = match Fat::abrir(&f, 0) {
413 Err(e) => e,
414 Ok(_) => panic!("un BPB de ceros NO puede abrirse como FAT"),
415 };
416 assert!(
417 e.to_string().contains("BPB imposible"),
418 "un BPB de ceros tiene que fallar, no leerse como «FAT llena»: {e}"
419 );
420 // Y sin la firma tampoco.
421 std::fs::write(&f, vec![0u8; 512 * 64]).unwrap();
422 assert!(Fat::abrir(&f, 0).is_err(), "sin 0x55AA no hay volumen");
423 std::fs::remove_dir_all(&tmp).ok();
424 }
425
426 /// Fabrica una entrada LFN + su 8.3, como las escribe Windows.
427 fn entradas_lfn(largo: &str, corto: &[u8; 11], dir: bool) -> Vec<u8> {
428 let u: Vec<u16> = largo.encode_utf16().collect();
429 let mut out = Vec::new();
430 let trozos: Vec<&[u16]> = u.chunks(13).collect();
431 // Los LFN se escriben AL REVÉS, con el último marcado 0x40.
432 for (i, t) in trozos.iter().enumerate().rev() {
433 let mut e = [0u8; 32];
434 e[0] = (i as u8 + 1) | if i == trozos.len() - 1 { 0x40 } else { 0 };
435 e[0x0B] = 0x0F;
436 let mut rel = Vec::from(*t);
437 while rel.len() < 13 {
438 rel.push(if rel.len() == t.len() { 0 } else { 0xFFFF });
439 }
440 let mut k = 0;
441 for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] {
442 for j in 0..n {
443 e[o + j * 2..o + j * 2 + 2].copy_from_slice(&rel[k].to_le_bytes());
444 k += 1;
445 }
446 }
447 out.extend_from_slice(&e);
448 }
449 let mut e = [0u8; 32];
450 e[0..11].copy_from_slice(corto);
451 e[0x0B] = if dir { 0x10 } else { 0x20 };
452 out.extend_from_slice(&e);
453 out
454 }
455
456 #[test]
457 fn el_nombre_largo_gana_al_8_3() {
458 // Sin juntar los LFN, «Microsoft» se vería como «MICROS~1» y la advertencia de vecino
459 // Windows —que es la razón de ser del §4— no dispararía nunca.
460 let mut d = entradas_lfn("Microsoft", b"MICROS~1 ", true);
461 d.extend(entradas_lfn("takana", b"TAKANA ", true));
462 d.extend_from_slice(&[0u8; 32]); // fin de directorio
463 let v = parse_dir(&d);
464 assert_eq!(v.len(), 2, "dos directorios: {v:?}");
465 assert_eq!(v[0].nombre, "Microsoft", "el LFN manda sobre el 8.3");
466 assert!(v[0].es_dir);
467 assert_eq!(v[1].nombre, "takana");
468 }
469
470 #[test]
471 fn sin_lfn_se_usa_el_8_3_y_se_saltean_las_borradas() {
472 let mut d = Vec::new();
473 let mut e = [0u8; 32];
474 e[0..11].copy_from_slice(b"BOOT ");
475 e[0x0B] = 0x10;
476 d.extend_from_slice(&e);
477 let mut borrada = [0u8; 32];
478 borrada[0] = 0xE5; // borrada: no debe aparecer
479 borrada[1..11].copy_from_slice(b"ORRADA ");
480 d.extend_from_slice(&borrada);
481 let mut vol = [0u8; 32];
482 vol[0..11].copy_from_slice(b"ESP ");
483 vol[0x0B] = 0x08; // etiqueta de volumen: tampoco es una entrada
484 d.extend_from_slice(&vol);
485 d.extend_from_slice(&[0u8; 32]);
486 let v = parse_dir(&d);
487 assert_eq!(v.len(), 1, "sólo BOOT: {v:?}");
488 assert_eq!(v[0].nombre, "BOOT");
489 }
490
491 #[test]
492 fn el_tipo_sale_del_numero_de_clusters_y_no_del_texto_del_bpb() {
493 // Un volumen chico es FAT16 aunque el BPB diga "FAT32 ": el texto es informativo y hay
494 // formateadores que mienten. La cuenta de clusters es la que manda (spec de Microsoft).
495 let tmp = std::env::temp_dir().join(format!("takana-fat-tipo-{}", std::process::id()));
496 std::fs::create_dir_all(&tmp).unwrap();
497 // 8192 sectores ⇒ (8192 − 49) = 8143 clusters, que cae en la franja FAT16 (4085..65525).
498 // El primer intento de este test usaba 2048 sectores = 1999 clusters, que es FAT12 de
499 // verdad: el que estaba mal era el TEST, no el código. Queda anotado porque la franja se
500 // olvida y el error se ve idéntico a un bug del lector.
501 let mut d = vec![0u8; 512 * 8192];
502 d[0x0B..0x0D].copy_from_slice(&512u16.to_le_bytes());
503 d[0x0D] = 1;
504 d[0x0E..0x10].copy_from_slice(&1u16.to_le_bytes());
505 d[0x10] = 2;
506 d[0x11..0x13].copy_from_slice(&512u16.to_le_bytes());
507 d[0x13..0x15].copy_from_slice(&8192u16.to_le_bytes());
508 d[0x16..0x18].copy_from_slice(&8u16.to_le_bytes());
509 d[0x52..0x5A].copy_from_slice(b"FAT32 "); // la mentira
510 d[510] = 0x55;
511 d[511] = 0xAA;
512 let f = tmp.join("chico.img");
513 std::fs::write(&f, &d).unwrap();
514 let fat = Fat::abrir(&f, 0).unwrap();
515 assert_eq!(fat.info.tipo, 16, "≈2000 clusters es FAT16, diga lo que diga el texto");
516 std::fs::remove_dir_all(&tmp).ok();
517 }
518}