//! Lector FAT de **sólo lectura**, para mirar una ESP sin montarla (ADR 0018 §4). //! //! ── POR QUÉ NO SE MONTA Y YA ──────────────────────────────────────────────────────────────────── //! El §4 pide que el instalador reporte el terreno **antes de tocar nada**: qué hay en la ESP, //! cuánto espacio queda y con quién se comparte. Montar para averiguarlo tiene tres problemas: pide //! privilegios, deja un efecto secundario justo cuando prometimos no tocar nada, y falla feo si el //! sistema operativo del vecino dejó la FAT sucia por su hibernación (el «Fast Startup» de Windows). //! //! Leer la estructura es más barato que todo eso: el BPB da la geometría, la FAT da los clusters //! libres y el directorio da los vendors. Nada de esto escribe un byte. //! //! ⚠ Sólo entiende lo que hace falta y lo dice cuando no entiende algo: FAT12/16/32 para la //! geometría, nombres 8.3 y LFN para el listado. No hay escritura ni la va a haber — para eso está //! mtools, que ya se usa en los scripts de imagen. use std::io::{self, Read, Seek, SeekFrom}; use std::path::Path; /// Geometría y ocupación de un volumen FAT. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FatInfo { pub bytes_por_sector: u32, pub sectores_por_cluster: u32, pub total_sectores: u64, pub clusters_totales: u64, pub clusters_libres: u64, /// 12, 16 o 32. pub tipo: u8, pub etiqueta: String, } impl FatInfo { pub fn bytes_totales(&self) -> u64 { self.total_sectores * self.bytes_por_sector as u64 } pub fn bytes_libres(&self) -> u64 { self.clusters_libres * self.sectores_por_cluster as u64 * self.bytes_por_sector as u64 } pub fn bytes_usados(&self) -> u64 { let cl_usados = self.clusters_totales.saturating_sub(self.clusters_libres); cl_usados * self.sectores_por_cluster as u64 * self.bytes_por_sector as u64 } } /// Una entrada de directorio, ya resuelta a nombre legible. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Entrada { pub nombre: String, pub es_dir: bool, pub tam: u32, } fn le16(b: &[u8], o: usize) -> u32 { u16::from_le_bytes([b[o], b[o + 1]]) as u32 } fn le32(b: &[u8], o: usize) -> u32 { u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) } /// Abre el volumen FAT que empieza en `offset` bytes dentro de `dev`. pub struct Fat { f: std::fs::File, base: u64, pub info: FatInfo, fat_inicio: u64, datos_inicio: u64, root_cluster: u32, /// FAT12/16: el root es un área fija, no una cadena de clusters. root_fijo: Option<(u64, u32)>, } impl Fat { pub fn abrir(dev: &Path, offset: u64) -> io::Result { let mut f = std::fs::File::open(dev)?; let mut bpb = [0u8; 512]; f.seek(SeekFrom::Start(offset))?; f.read_exact(&mut bpb)?; if bpb[510] != 0x55 || bpb[511] != 0xAA { return Err(io::Error::new( io::ErrorKind::InvalidData, "no parece un volumen FAT: falta la firma 0x55AA", )); } let bps = le16(&bpb, 0x0B); let spc = bpb[0x0D] as u32; // Un BPB con ceros acá no es «FAT vacía»: es que no estamos mirando un BPB. if !(512..=4096).contains(&bps) || !bps.is_power_of_two() || spc == 0 || !spc.is_power_of_two() { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("BPB imposible: {bps} B/sector, {spc} sectores/cluster"), )); } let reservados = le16(&bpb, 0x0E); let n_fats = bpb[0x10] as u32; let root_entries = le16(&bpb, 0x11); let tot16 = le16(&bpb, 0x13) as u64; let fat16 = le16(&bpb, 0x16) as u64; let tot32 = le32(&bpb, 0x20) as u64; let fat32 = le32(&bpb, 0x24) as u64; let total_sectores = if tot16 != 0 { tot16 } else { tot32 }; let fat_sectores = if fat16 != 0 { fat16 } else { fat32 }; if n_fats == 0 || fat_sectores == 0 || total_sectores == 0 { return Err(io::Error::new(io::ErrorKind::InvalidData, "BPB sin FAT ni sectores")); } // El área del root es fija en FAT12/16 y una cadena de clusters en FAT32. let root_sectores = (root_entries * 32).div_ceil(bps) as u64; let primer_dato = reservados as u64 + n_fats as u64 * fat_sectores + root_sectores; let clusters_totales = (total_sectores - primer_dato) / spc as u64; // El tipo se decide POR EL NÚMERO DE CLUSTERS, no por la cadena de texto del BPB: esa es // informativa y hay formateadores que mienten en ella. let tipo = if clusters_totales < 4085 { 12 } else if clusters_totales < 65525 { 16 } else { 32 }; let etiqueta = { let off = if tipo == 32 { 0x47 } else { 0x2B }; String::from_utf8_lossy(&bpb[off..off + 11]).trim().to_string() }; let fat_inicio = offset + reservados as u64 * bps as u64; let datos_inicio = offset + primer_dato * bps as u64; let root_fijo = if tipo == 32 { None } else { Some(( offset + (reservados as u64 + n_fats as u64 * fat_sectores) * bps as u64, root_entries, )) }; let mut fat = Fat { f, base: offset, info: FatInfo { bytes_por_sector: bps, sectores_por_cluster: spc, total_sectores, clusters_totales, clusters_libres: 0, tipo, etiqueta, }, fat_inicio, datos_inicio, root_cluster: if tipo == 32 { le32(&bpb, 0x2C) } else { 0 }, root_fijo, }; fat.info.clusters_libres = fat.contar_libres()?; Ok(fat) } /// Cuenta clusters libres recorriendo la FAT. **No se usa el `FSInfo`** de FAT32 a propósito: /// ese campo es una pista que el sistema operativo que desmontó mal deja desactualizada, y acá /// el número se usa para decidir si una instalación entra — un número optimista de más haría /// fallar la instalación a mitad, que es justo lo que el §4 existe para evitar. fn contar_libres(&mut self) -> io::Result { let n = self.info.clusters_totales; let mut libres = 0u64; match self.info.tipo { 32 => { let mut buf = vec![0u8; 4096]; let mut leidos = 0u64; self.f.seek(SeekFrom::Start(self.fat_inicio))?; // Los clusters 0 y 1 son reservados: la cuenta útil empieza en 2. while leidos < n + 2 { let k = self.f.read(&mut buf)?; if k == 0 { break; } for c in buf[..k].chunks_exact(4) { if leidos >= 2 && leidos < n + 2 && le32(c, 0) & 0x0FFF_FFFF == 0 { libres += 1; } leidos += 1; } } } 16 => { let mut buf = vec![0u8; 4096]; let mut leidos = 0u64; self.f.seek(SeekFrom::Start(self.fat_inicio))?; while leidos < n + 2 { let k = self.f.read(&mut buf)?; if k == 0 { break; } for c in buf[..k].chunks_exact(2) { if leidos >= 2 && leidos < n + 2 && le16(c, 0) == 0 { libres += 1; } leidos += 1; } } } _ => { // FAT12 son entradas de 12 bits a caballo entre bytes. Una ESP nunca es FAT12, así // que en vez de implementarlo a medias se declara desconocido: un 0 aquí se leería // como «llena» y eso es una mentira con consecuencias. return Ok(0); } } Ok(libres) } fn siguiente_cluster(&mut self, c: u32) -> io::Result> { let (off, val) = match self.info.tipo { 32 => { let mut b = [0u8; 4]; self.f.seek(SeekFrom::Start(self.fat_inicio + c as u64 * 4))?; self.f.read_exact(&mut b)?; (0, le32(&b, 0) & 0x0FFF_FFFF) } 16 => { let mut b = [0u8; 2]; self.f.seek(SeekFrom::Start(self.fat_inicio + c as u64 * 2))?; self.f.read_exact(&mut b)?; (0, le16(&b, 0)) } _ => return Ok(None), }; let _ = off; let fin = if self.info.tipo == 32 { 0x0FFF_FFF8 } else { 0xFFF8 }; if val >= fin || val < 2 { Ok(None) } else { Ok(Some(val)) } } fn leer_cluster(&mut self, c: u32) -> io::Result> { let tam = self.info.sectores_por_cluster as usize * self.info.bytes_por_sector as usize; let mut v = vec![0u8; tam]; let off = self.datos_inicio + (c as u64 - 2) * tam as u64; self.f.seek(SeekFrom::Start(off))?; self.f.read_exact(&mut v)?; Ok(v) } /// Entradas del directorio raíz. pub fn raiz(&mut self) -> io::Result> { if let Some((off, n)) = self.root_fijo { let mut v = vec![0u8; n as usize * 32]; self.f.seek(SeekFrom::Start(off))?; self.f.read_exact(&mut v)?; return Ok(parse_dir(&v)); } self.leer_cadena(self.root_cluster).map(|b| parse_dir(&b)) } /// Entradas de un subdirectorio del raíz (un solo nivel, que es lo que el §4 necesita: ver los /// *vendors* que cuelgan de `\EFI`). pub fn subdir(&mut self, nombre: &str) -> io::Result>> { let dir = if let Some((off, n)) = self.root_fijo { let mut v = vec![0u8; n as usize * 32]; self.f.seek(SeekFrom::Start(off))?; self.f.read_exact(&mut v)?; v } else { self.leer_cadena(self.root_cluster)? }; let Some(c) = buscar_cluster(&dir, nombre) else { return Ok(None) }; if c < 2 { return Ok(None); } Ok(Some(parse_dir(&self.leer_cadena(c)?))) } /// Lee un fichero por ruta absoluta dentro del volumen (`/EFI/takana/takanax64.efi`). /// /// Trunca al **tamaño declarado** en la entrada de directorio, no al final del último cluster: /// un fichero de 15 MiB en clusters de 1 KiB termina con relleno, y hashear el relleno daría un /// hash distinto al del mismo fichero en disco. Es el tipo de diferencia que sólo se descubre /// cuando dos respaldos del mismo arranque no coinciden. pub fn leer_archivo(&mut self, ruta: &str) -> io::Result>> { let partes: Vec<&str> = ruta.split('/').filter(|x| !x.is_empty()).collect(); let Some((archivo, dirs)) = partes.split_last() else { return Ok(None) }; let mut dir = if let Some((off, n)) = self.root_fijo { let mut v = vec![0u8; n as usize * 32]; self.f.seek(SeekFrom::Start(off))?; self.f.read_exact(&mut v)?; v } else { self.leer_cadena(self.root_cluster)? }; for d in dirs { let Some(c) = buscar_cluster(&dir, d) else { return Ok(None) }; if c < 2 { return Ok(None); } dir = self.leer_cadena(c)?; } let Some((cluster, tam)) = buscar_entrada(&dir, archivo) else { return Ok(None) }; if cluster < 2 || tam == 0 { return Ok(Some(Vec::new())); } let mut b = self.leer_cadena(cluster)?; b.truncate(tam as usize); Ok(Some(b)) } /// Todos los ficheros del volumen hasta `prof` niveles, como rutas absolutas. Acotado a /// propósito: una ESP es plana por convención y un recorrido sin tope se cuelga con un /// directorio corrupto que se apunta a sí mismo. pub fn listar_recursivo(&mut self, prof: u32) -> io::Result> { let mut out = Vec::new(); let raiz = self.raiz()?; self.recorrer(&raiz, "", prof, &mut out)?; out.sort(); Ok(out) } fn recorrer( &mut self, entradas: &[Entrada], prefijo: &str, prof: u32, out: &mut Vec<(String, u32)>, ) -> io::Result<()> { for e in entradas { let ruta = format!("{prefijo}/{}", e.nombre); if e.es_dir { if prof == 0 { continue; } // Releer el directorio por su ruta, que es lo que `subdir` no da más allá del raíz. let hijos = self.listar_ruta(&ruta)?; self.recorrer(&hijos, &ruta, prof - 1, out)?; } else { out.push((ruta, e.tam)); } } Ok(()) } /// Entradas del directorio en `ruta` (absoluta dentro del volumen). pub fn listar_ruta(&mut self, ruta: &str) -> io::Result> { let mut dir = if let Some((off, n)) = self.root_fijo { let mut v = vec![0u8; n as usize * 32]; self.f.seek(SeekFrom::Start(off))?; self.f.read_exact(&mut v)?; v } else { self.leer_cadena(self.root_cluster)? }; for d in ruta.split('/').filter(|x| !x.is_empty()) { let Some(c) = buscar_cluster(&dir, d) else { return Ok(Vec::new()) }; if c < 2 { return Ok(Vec::new()); } dir = self.leer_cadena(c)?; } Ok(parse_dir(&dir)) } fn leer_cadena(&mut self, inicio: u32) -> io::Result> { let mut out = Vec::new(); let mut c = inicio; // Tope duro: una cadena circular por corrupción colgaría el instalador, y colgar es peor // que reportar de menos. for _ in 0..65536 { out.extend(self.leer_cluster(c)?); match self.siguiente_cluster(c)? { Some(n) => c = n, None => break, } } Ok(out) } pub fn base(&self) -> u64 { self.base } } fn nombre_83(e: &[u8]) -> String { let base = String::from_utf8_lossy(&e[0..8]).trim_end().to_string(); let ext = String::from_utf8_lossy(&e[8..11]).trim_end().to_string(); if ext.is_empty() { base } else { format!("{base}.{ext}") } } /// Decodifica un área de entradas de directorio. Junta los LFN (nombres largos) cuando están, que /// es lo normal en una ESP escrita por Windows: sin esto `Microsoft` se vería como `MICROS~1`. fn parse_dir(b: &[u8]) -> Vec { let mut out = Vec::new(); let mut lfn: Vec<(u8, Vec)> = Vec::new(); for e in b.chunks_exact(32) { match e[0] { 0x00 => break, // fin del directorio 0xE5 => { lfn.clear(); continue; } // borrada _ => {} } let attr = e[0x0B]; if attr & 0x0F == 0x0F { // Entrada LFN: 13 unidades UTF-16 repartidas en tres tramos. let mut u = Vec::with_capacity(13); for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] { for i in 0..n { u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]])); } } lfn.push((e[0] & 0x3F, u)); continue; } if attr & 0x08 != 0 { lfn.clear(); continue; // etiqueta de volumen } let nombre = if lfn.is_empty() { nombre_83(e) } else { lfn.sort_by_key(|(n, _)| *n); let mut u: Vec = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect(); if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) { u.truncate(p); } String::from_utf16_lossy(&u) }; lfn.clear(); let nombre = nombre.trim().to_string(); if nombre == "." || nombre == ".." || nombre.is_empty() { continue; } out.push(Entrada { nombre, es_dir: attr & 0x10 != 0, tam: le32(e, 0x1C) }); } out } /// Como `buscar_cluster` pero devuelve `(cluster, tamaño)`: para un fichero el tamaño es /// imprescindible, porque la cadena de clusters siempre da de más. fn buscar_entrada(dir: &[u8], nombre: &str) -> Option<(u32, u32)> { let mut lfn: Vec<(u8, Vec)> = Vec::new(); for e in dir.chunks_exact(32) { if e[0] == 0x00 { break; } if e[0] == 0xE5 { lfn.clear(); continue; } let attr = e[0x0B]; if attr & 0x0F == 0x0F { let mut u = Vec::with_capacity(13); for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] { for i in 0..n { u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]])); } } lfn.push((e[0] & 0x3F, u)); continue; } let corto = nombre_83(e); let largo = if lfn.is_empty() { String::new() } else { lfn.sort_by_key(|(n, _)| *n); let mut u: Vec = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect(); if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) { u.truncate(p); } String::from_utf16_lossy(&u) }; lfn.clear(); if corto.eq_ignore_ascii_case(nombre) || largo.eq_ignore_ascii_case(nombre) { return Some(((le16(e, 0x14) << 16) | le16(e, 0x1A), le32(e, 0x1C))); } } None } fn buscar_cluster(dir: &[u8], nombre: &str) -> Option { let mut lfn: Vec<(u8, Vec)> = Vec::new(); for e in dir.chunks_exact(32) { if e[0] == 0x00 { break; } if e[0] == 0xE5 { lfn.clear(); continue; } let attr = e[0x0B]; if attr & 0x0F == 0x0F { let mut u = Vec::with_capacity(13); for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] { for i in 0..n { u.push(u16::from_le_bytes([e[o + i * 2], e[o + i * 2 + 1]])); } } lfn.push((e[0] & 0x3F, u)); continue; } let corto = nombre_83(e); let largo = if lfn.is_empty() { String::new() } else { lfn.sort_by_key(|(n, _)| *n); let mut u: Vec = lfn.iter().flat_map(|(_, x)| x.iter().copied()).collect(); if let Some(p) = u.iter().position(|&c| c == 0 || c == 0xFFFF) { u.truncate(p); } String::from_utf16_lossy(&u) }; lfn.clear(); if corto.eq_ignore_ascii_case(nombre) || largo.eq_ignore_ascii_case(nombre) { return Some((le16(e, 0x14) << 16) | le16(e, 0x1A)); } } None } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; /// BPB fabricado a mano: prueba la geometría y, sobre todo, que un BPB imposible se RECHACE. #[test] fn rechaza_un_bpb_imposible_en_vez_de_devolver_ceros() { let tmp = std::env::temp_dir().join(format!("takana-fat-neg-{}", std::process::id())); std::fs::create_dir_all(&tmp).unwrap(); // Todo ceros salvo la firma: bytes/sector = 0 ⇒ no es un BPB. let mut d = vec![0u8; 512 * 64]; d[510] = 0x55; d[511] = 0xAA; let f = tmp.join("falso.img"); std::fs::write(&f, &d).unwrap(); let e = match Fat::abrir(&f, 0) { Err(e) => e, Ok(_) => panic!("un BPB de ceros NO puede abrirse como FAT"), }; assert!( e.to_string().contains("BPB imposible"), "un BPB de ceros tiene que fallar, no leerse como «FAT llena»: {e}" ); // Y sin la firma tampoco. std::fs::write(&f, vec![0u8; 512 * 64]).unwrap(); assert!(Fat::abrir(&f, 0).is_err(), "sin 0x55AA no hay volumen"); std::fs::remove_dir_all(&tmp).ok(); } /// Fabrica una entrada LFN + su 8.3, como las escribe Windows. fn entradas_lfn(largo: &str, corto: &[u8; 11], dir: bool) -> Vec { let u: Vec = largo.encode_utf16().collect(); let mut out = Vec::new(); let trozos: Vec<&[u16]> = u.chunks(13).collect(); // Los LFN se escriben AL REVÉS, con el último marcado 0x40. for (i, t) in trozos.iter().enumerate().rev() { let mut e = [0u8; 32]; e[0] = (i as u8 + 1) | if i == trozos.len() - 1 { 0x40 } else { 0 }; e[0x0B] = 0x0F; let mut rel = Vec::from(*t); while rel.len() < 13 { rel.push(if rel.len() == t.len() { 0 } else { 0xFFFF }); } let mut k = 0; for &(o, n) in &[(1usize, 5usize), (14, 6), (28, 2)] { for j in 0..n { e[o + j * 2..o + j * 2 + 2].copy_from_slice(&rel[k].to_le_bytes()); k += 1; } } out.extend_from_slice(&e); } let mut e = [0u8; 32]; e[0..11].copy_from_slice(corto); e[0x0B] = if dir { 0x10 } else { 0x20 }; out.extend_from_slice(&e); out } /// Fabrica una FAT16 mínima y válida con UN fichero de contenido conocido, sin depender de /// mtools: así el test corre en cualquier entorno y no se saltea en silencio. fn fat16_con_fichero(tmp: &Path, nombre_83: &[u8; 11], datos: &[u8]) -> PathBuf { let bps = 512usize; let reservados = 1usize; let n_fats = 1usize; let fat_sectores = 32usize; let root_entries = 16usize; let total = 8192usize; // ⇒ 8158 clusters: franja FAT16 let mut d = vec![0u8; bps * total]; d[0x0B..0x0D].copy_from_slice(&(bps as u16).to_le_bytes()); d[0x0D] = 1; // sectores por cluster d[0x0E..0x10].copy_from_slice(&(reservados as u16).to_le_bytes()); d[0x10] = n_fats as u8; d[0x11..0x13].copy_from_slice(&(root_entries as u16).to_le_bytes()); d[0x13..0x15].copy_from_slice(&(total as u16).to_le_bytes()); d[0x16..0x18].copy_from_slice(&(fat_sectores as u16).to_le_bytes()); d[0x2B..0x36].copy_from_slice(b"PRUEBA "); d[510] = 0x55; d[511] = 0xAA; // FAT: clusters 0 y 1 reservados; el 2 es el fin de la cadena de nuestro fichero. let fat = reservados * bps; d[fat..fat + 2].copy_from_slice(&0xFFF8u16.to_le_bytes()); d[fat + 2..fat + 4].copy_from_slice(&0xFFFFu16.to_le_bytes()); d[fat + 4..fat + 6].copy_from_slice(&0xFFFFu16.to_le_bytes()); // Directorio raíz: un fichero en el cluster 2. let root = (reservados + n_fats * fat_sectores) * bps; d[root..root + 11].copy_from_slice(nombre_83); d[root + 0x0B] = 0x20; // archivo d[root + 0x1A..root + 0x1C].copy_from_slice(&2u16.to_le_bytes()); d[root + 0x1C..root + 0x20].copy_from_slice(&(datos.len() as u32).to_le_bytes()); // Datos: el cluster 2 es el primer sector de datos. let primer_dato = (reservados + n_fats * fat_sectores + root_entries * 32 / bps) * bps; d[primer_dato..primer_dato + datos.len()].copy_from_slice(datos); let f = tmp.join("fat16.img"); std::fs::write(&f, &d).unwrap(); f } #[test] fn lee_un_fichero_entero_y_lo_trunca_al_tamano_declarado() { let tmp = std::env::temp_dir().join(format!("takana-fat-file-{}", std::process::id())); std::fs::create_dir_all(&tmp).unwrap(); // 300 bytes en un cluster de 512: el resto del cluster es RELLENO. Si no se trunca al // tamaño declarado, el hash del respaldo del ADR 0018 §5 no coincidiría nunca con el del // mismo fichero en disco — y el síntoma sería «dos respaldos del mismo arranque difieren». let datos: Vec = (0..300u32).map(|i| (i % 251) as u8).collect(); let img = fat16_con_fichero(&tmp, b"KERNEL EFI", &datos); let mut fat = Fat::abrir(&img, 0).unwrap(); assert_eq!(fat.info.tipo, 16); let leido = fat.leer_archivo("/KERNEL.EFI").unwrap().expect("el fichero está"); assert_eq!(leido.len(), 300, "truncado al tamaño declarado, no al del cluster"); assert_eq!(leido, datos, "y los bytes son exactamente los que se escribieron"); // Un fichero que no está devuelve None, no un vector vacío que se confundiría con «vacío». assert!(fat.leer_archivo("/NO-ESTA.EFI").unwrap().is_none()); assert!(fat.leer_archivo("/SUB/NO-ESTA.EFI").unwrap().is_none()); let l = fat.listar_recursivo(3).unwrap(); assert_eq!(l, vec![("/KERNEL.EFI".to_string(), 300u32)]); std::fs::remove_dir_all(&tmp).ok(); } #[test] fn el_nombre_largo_gana_al_8_3() { // Sin juntar los LFN, «Microsoft» se vería como «MICROS~1» y la advertencia de vecino // Windows —que es la razón de ser del §4— no dispararía nunca. let mut d = entradas_lfn("Microsoft", b"MICROS~1 ", true); d.extend(entradas_lfn("takana", b"TAKANA ", true)); d.extend_from_slice(&[0u8; 32]); // fin de directorio let v = parse_dir(&d); assert_eq!(v.len(), 2, "dos directorios: {v:?}"); assert_eq!(v[0].nombre, "Microsoft", "el LFN manda sobre el 8.3"); assert!(v[0].es_dir); assert_eq!(v[1].nombre, "takana"); } #[test] fn sin_lfn_se_usa_el_8_3_y_se_saltean_las_borradas() { let mut d = Vec::new(); let mut e = [0u8; 32]; e[0..11].copy_from_slice(b"BOOT "); e[0x0B] = 0x10; d.extend_from_slice(&e); let mut borrada = [0u8; 32]; borrada[0] = 0xE5; // borrada: no debe aparecer borrada[1..11].copy_from_slice(b"ORRADA "); d.extend_from_slice(&borrada); let mut vol = [0u8; 32]; vol[0..11].copy_from_slice(b"ESP "); vol[0x0B] = 0x08; // etiqueta de volumen: tampoco es una entrada d.extend_from_slice(&vol); d.extend_from_slice(&[0u8; 32]); let v = parse_dir(&d); assert_eq!(v.len(), 1, "sólo BOOT: {v:?}"); assert_eq!(v[0].nombre, "BOOT"); } #[test] fn el_tipo_sale_del_numero_de_clusters_y_no_del_texto_del_bpb() { // Un volumen chico es FAT16 aunque el BPB diga "FAT32 ": el texto es informativo y hay // formateadores que mienten. La cuenta de clusters es la que manda (spec de Microsoft). let tmp = std::env::temp_dir().join(format!("takana-fat-tipo-{}", std::process::id())); std::fs::create_dir_all(&tmp).unwrap(); // 8192 sectores ⇒ (8192 − 49) = 8143 clusters, que cae en la franja FAT16 (4085..65525). // El primer intento de este test usaba 2048 sectores = 1999 clusters, que es FAT12 de // verdad: el que estaba mal era el TEST, no el código. Queda anotado porque la franja se // olvida y el error se ve idéntico a un bug del lector. let mut d = vec![0u8; 512 * 8192]; d[0x0B..0x0D].copy_from_slice(&512u16.to_le_bytes()); d[0x0D] = 1; d[0x0E..0x10].copy_from_slice(&1u16.to_le_bytes()); d[0x10] = 2; d[0x11..0x13].copy_from_slice(&512u16.to_le_bytes()); d[0x13..0x15].copy_from_slice(&8192u16.to_le_bytes()); d[0x16..0x18].copy_from_slice(&8u16.to_le_bytes()); d[0x52..0x5A].copy_from_slice(b"FAT32 "); // la mentira d[510] = 0x55; d[511] = 0xAA; let f = tmp.join("chico.img"); std::fs::write(&f, &d).unwrap(); let fat = Fat::abrir(&f, 0).unwrap(); assert_eq!(fat.info.tipo, 16, "≈2000 clusters es FAT16, diga lo que diga el texto"); std::fs::remove_dir_all(&tmp).ok(); } }