cache.rs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. use std::fs::{self, File};
  2. use std::io::{self, BufReader, BufWriter, Read, Write};
  3. use std::path::Path;
  4. use std::time::SystemTime;
  5. use memmap2::Mmap;
  6. use super::UniqueLen;
  7. /// Magic + version guard
  8. const MAGIC: &[u8; 8] = b"UNIQIDX1";
  9. const VERSION: u32 = 2;
  10. const HEADER_LEN: usize = 20;
  11. /// Identifies a cache entry unambiguously.
  12. /// Derive from FASTA path + contig name + genome length + content hash.
  13. pub struct CacheKey {
  14. pub contig: String,
  15. pub genome_len: usize,
  16. /// Blake3 hex hash of the raw genome bytes (first 16 hex chars sufficient)
  17. pub hash: String,
  18. }
  19. impl CacheKey {
  20. /// Compute from raw genome bytes + contig name.
  21. pub fn from_genome(contig: &str, genome: &[u8]) -> Self {
  22. // Blake3 is fast (~3 GB/s), ideal for large genomes.
  23. // Use `blake3` crate or fall back to a simple FNV for no-dep builds.
  24. let hash = blake3_hex_short(genome);
  25. Self {
  26. contig: contig.to_owned(),
  27. genome_len: genome.len(),
  28. hash,
  29. }
  30. }
  31. /// Compute a genome-wide cache key without reading the FASTA sequence.
  32. ///
  33. /// `metadata` should include all selected contig names/lengths and any
  34. /// indexing parameters that affect the cached vectors.
  35. pub fn from_genome_metadata(
  36. contig: &str,
  37. genome_len: usize,
  38. fasta_path: &Path,
  39. metadata: &[u8],
  40. ) -> io::Result<Self> {
  41. let meta = std::fs::metadata(fasta_path)?;
  42. let mtime = meta
  43. .modified()?
  44. .duration_since(SystemTime::UNIX_EPOCH)
  45. .map(|d| d.as_secs())
  46. .unwrap_or(0);
  47. let fsize = meta.len();
  48. let mut hash_input = Vec::with_capacity(metadata.len() + 32);
  49. hash_input.extend_from_slice(&mtime.to_le_bytes());
  50. hash_input.extend_from_slice(&fsize.to_le_bytes());
  51. hash_input.extend_from_slice(metadata);
  52. Ok(Self {
  53. contig: contig.to_owned(),
  54. genome_len,
  55. hash: blake3_hex_short(&hash_input),
  56. })
  57. }
  58. /// Construit la clé depuis le .fai (noodles) — ne lit pas le FASTA.
  59. /// `fai_entry` contient déjà la longueur du contig.
  60. pub fn from_fai(contig: &str, fai_len: usize, fasta_path: &Path) -> io::Result<Self> {
  61. let meta = std::fs::metadata(fasta_path)?;
  62. let mtime = meta
  63. .modified()?
  64. .duration_since(SystemTime::UNIX_EPOCH)
  65. .map(|d| d.as_secs())
  66. .unwrap_or(0);
  67. let fsize = meta.len();
  68. // Hash = mtime + fsize — suffisant pour invalider sur modification
  69. let hash = format!("{mtime:016x}{fsize:016x}");
  70. Ok(Self {
  71. contig: contig.to_owned(),
  72. genome_len: fai_len,
  73. hash,
  74. })
  75. }
  76. pub fn file_name(&self) -> String {
  77. format!(
  78. "{}__{}__{}.uniqidx",
  79. self.contig, self.genome_len, self.hash
  80. )
  81. }
  82. }
  83. /// Binary format (little-endian):
  84. ///
  85. /// [0..8] magic = b"UNIQIDX1"
  86. /// [8..12] version = 2u32
  87. /// [12..20] n = genome_len as u64
  88. /// [20..] min_unique_len: n × u32 (`NOT_UNIQUE` means no unique interval)
  89. pub fn save_to_cache(path: &Path, min_unique_len: &[UniqueLen]) -> io::Result<()> {
  90. let tmp = path.with_extension("uniqidx.tmp");
  91. {
  92. let f = File::create(&tmp)?;
  93. let mut w = BufWriter::with_capacity(8 * 1024 * 1024, f);
  94. w.write_all(MAGIC)?;
  95. w.write_all(&VERSION.to_le_bytes())?;
  96. w.write_all(&(min_unique_len.len() as u64).to_le_bytes())?;
  97. // Bulk-write as u32 array — avoids per-element call overhead.
  98. let buf: Vec<u8> = min_unique_len
  99. .iter()
  100. .flat_map(|&v| v.to_le_bytes())
  101. .collect();
  102. w.write_all(&buf)?;
  103. w.flush()?;
  104. }
  105. // Atomic rename: avoids corrupt cache on crash mid-write
  106. fs::rename(&tmp, path)?;
  107. Ok(())
  108. }
  109. pub fn load_from_cache(path: &Path, expected_len: usize) -> io::Result<Vec<UniqueLen>> {
  110. let f = File::open(path)?;
  111. let mut r = BufReader::with_capacity(8 * 1024 * 1024, f);
  112. // Magic
  113. let mut magic = [0u8; 8];
  114. r.read_exact(&mut magic)?;
  115. if &magic != MAGIC {
  116. return Err(io::Error::new(io::ErrorKind::InvalidData, "bad magic"));
  117. }
  118. // Version
  119. let mut vbuf = [0u8; 4];
  120. r.read_exact(&mut vbuf)?;
  121. if u32::from_le_bytes(vbuf) != VERSION {
  122. return Err(io::Error::new(
  123. io::ErrorKind::InvalidData,
  124. "version mismatch",
  125. ));
  126. }
  127. // Length
  128. let mut nbuf = [0u8; 8];
  129. r.read_exact(&mut nbuf)?;
  130. let n = u64::from_le_bytes(nbuf) as usize;
  131. if n != expected_len {
  132. return Err(io::Error::new(
  133. io::ErrorKind::InvalidData,
  134. format!("length mismatch: cache has {n}, genome is {expected_len}"),
  135. ));
  136. }
  137. // Payload: read all at once then cast
  138. let byte_len = n * 4;
  139. let mut buf = vec![0u8; byte_len];
  140. r.read_exact(&mut buf)?;
  141. let result: Vec<UniqueLen> = buf
  142. .chunks_exact(4)
  143. .map(|c| u32::from_le_bytes(c.try_into().unwrap()))
  144. .collect();
  145. Ok(result)
  146. }
  147. pub(crate) struct MappedUniqueLenCache {
  148. mmap: Mmap,
  149. len: usize,
  150. }
  151. impl MappedUniqueLenCache {
  152. pub(crate) fn get(&self, index: usize) -> UniqueLen {
  153. assert!(index < self.len);
  154. let offset = HEADER_LEN + index * 4;
  155. UniqueLen::from_le_bytes(
  156. self.mmap[offset..offset + 4]
  157. .try_into()
  158. .expect("cache payload offset should be in bounds"),
  159. )
  160. }
  161. pub(crate) fn len(&self) -> usize {
  162. self.len
  163. }
  164. }
  165. pub(crate) fn mmap_from_cache(
  166. path: &Path,
  167. expected_len: usize,
  168. ) -> io::Result<MappedUniqueLenCache> {
  169. let f = File::open(path)?;
  170. let file_len = f.metadata()?.len() as usize;
  171. let expected_file_len =
  172. HEADER_LEN
  173. .checked_add(expected_len.checked_mul(4).ok_or_else(|| {
  174. io::Error::new(io::ErrorKind::InvalidInput, "cache length overflow")
  175. })?)
  176. .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "cache length overflow"))?;
  177. if file_len != expected_file_len {
  178. return Err(io::Error::new(
  179. io::ErrorKind::InvalidData,
  180. format!("file length mismatch: cache has {file_len}, expected {expected_file_len}"),
  181. ));
  182. }
  183. // SAFETY: The mapping is read-only and the file is never mutated through
  184. // this handle. Cache files are written atomically before being mapped.
  185. let mmap = unsafe { Mmap::map(&f)? };
  186. validate_header(&mmap[..HEADER_LEN], expected_len)?;
  187. Ok(MappedUniqueLenCache {
  188. mmap,
  189. len: expected_len,
  190. })
  191. }
  192. fn validate_header(header: &[u8], expected_len: usize) -> io::Result<()> {
  193. if &header[0..8] != MAGIC {
  194. return Err(io::Error::new(io::ErrorKind::InvalidData, "bad magic"));
  195. }
  196. let version = u32::from_le_bytes(header[8..12].try_into().unwrap());
  197. if version != VERSION {
  198. return Err(io::Error::new(
  199. io::ErrorKind::InvalidData,
  200. "version mismatch",
  201. ));
  202. }
  203. let n = u64::from_le_bytes(header[12..20].try_into().unwrap()) as usize;
  204. if n != expected_len {
  205. return Err(io::Error::new(
  206. io::ErrorKind::InvalidData,
  207. format!("length mismatch: cache has {n}, genome is {expected_len}"),
  208. ));
  209. }
  210. Ok(())
  211. }
  212. // ── Minimal Blake3-like hash (FNV-1a 64bit, no deps) ─────────────────────────
  213. // Replace with `blake3` crate for collision resistance on production.
  214. fn blake3_hex_short(data: &[u8]) -> String {
  215. const FNV_OFFSET: u64 = 14695981039346656037;
  216. const FNV_PRIME: u64 = 1099511628211;
  217. let mut h = FNV_OFFSET;
  218. for &b in data {
  219. h ^= b as u64;
  220. h = h.wrapping_mul(FNV_PRIME);
  221. }
  222. format!("{h:016x}")
  223. }