//! `cargo run -p benilla-formats --example chain_report [--toc | …]` — which archive //! the client will actually read a file from, and why. //! //! The question a realm update raises. The reference mounts `patch-?.MPQ` by case-folded name, so a //! letter patch outranks a digit patch ([`benilla_formats::Chain`]'s `mount_order`), and an update //! shipped as `patch-4`/`patch-5` lands *below* every `patch-A..Z` already on disk. One deviation //! corrects part of that — an archive whose `FrameXML.toc` is a strict subset of another's is //! demoted as a stale snapshot — but an archive carrying no toc at all is not a candidate for it, //! so after an update this is the measurement that says whether the new files are the ones being //! read. `chain_shadow` answers the same question from the archive's side. //! //! - no argument: the `FrameXML.toc` copies in priority order, which is the evidence the demotion //! keys on, plus the winner of a handful of files an update moves. //! - `--toc`: every row of the realm's own interface manifest with the archive that wins it, and a //! tally per archive. This is the one to read after an update: a row won by a *months-old* letter //! patch is a file the realm replaced and the client is not reading. //! - file names: the winner for each, as the client's backslash paths. //! - `--cat `: the **winning** copy's bytes on stdout, which is the copy the client reads. //! Reading a shipped file any other way — unzipping an archive by hand, or trusting a copy on //! disk — answers a different question than "what does the client see", and after an update //! those two answers diverge, which is the whole reason this example exists. fn main() { let Some(data) = benilla_formats::wow_data() else { eprintln!("no install"); std::process::exit(1); }; println!("data: {}", data.display()); let chain = benilla_formats::Chain::open(&data).expect("chain opens"); let toc = "Interface\\FrameXML\\FrameXML.toc"; let args: Vec = std::env::args().skip(1).collect(); if let Some(i) = args.iter().position(|a| a == "--cat") { let Some(name) = args.get(i + 1) else { eprintln!("--cat needs a file name"); std::process::exit(2); }; let name = name.replace('/', "\\"); match chain.read(&name) { Ok(bytes) => { eprintln!( "-- {name}: {} bytes, from {} --", bytes.len(), winner(&chain, &name) ); use std::io::Write; std::io::stdout().write_all(&bytes).expect("stdout"); } Err(e) => { eprintln!("{name}: not in the chain ({e})"); std::process::exit(1); } } return; } if args.iter().any(|a| a == "--toc") { let bytes = chain.read(toc).expect("the realm has a FrameXML.toc"); let rows: Vec = String::from_utf8_lossy(&bytes) .lines() .map(|l| l.trim().trim_start_matches('\u{feff}').to_string()) .filter(|l| !l.is_empty() && !l.starts_with('#')) .collect(); println!( "\n-- the realm's {} interface rows, and who wins each --", rows.len() ); let mut tally: std::collections::BTreeMap = Default::default(); for row in &rows { let name = format!("Interface\\FrameXML\\{}", row.replace('/', "\\")); let who = winner(&chain, &name); *tally.entry(who.clone()).or_default() += 1; println!(" {:<22} {row}", who); } println!("\n-- tally --"); for (who, n) in &tally { println!(" {who:<22} {n}"); } return; } println!("\n-- FrameXML.toc copies, ascending priority (last wins) --"); for (path, size) in chain.copies(toc) { println!(" {:<22} {size:>8} bytes", name_of(path)); } let files: Vec = if args.is_empty() { [ toc, "Interface\\FrameXML\\GlobalStrings.lua", "Interface\\FrameXML\\Minimap.lua", "Interface\\FrameXML\\TargetFrame.lua", "Interface\\GlueXML\\GlueXML.toc", "DBFilesClient\\Spell.dbc", "DBFilesClient\\AreaTable.dbc", "DBFilesClient\\Map.dbc", "DBFilesClient\\CreatureDisplayInfo.dbc", ] .iter() .map(|s| (*s).to_string()) .collect() } else { args }; println!("\n-- winners --"); for f in &files { println!( " {f:<46} {:<22} ({} copies)", winner(&chain, f), chain.copies(f).len() ); } } /// The archive the chain resolves `name` from, or `ABSENT`. fn winner(chain: &benilla_formats::Chain, name: &str) -> String { chain .find_file_archive(name) .map(|p| name_of(p).to_string()) .unwrap_or_else(|| "ABSENT".to_string()) } fn name_of(p: &std::path::Path) -> &str { p.file_name().and_then(|n| n.to_str()).unwrap_or("?") }