#![allow(clippy::unwrap_used)]
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use dxbc_reader::{Options, disassemble_with, parts};
use proptest::prelude::*;
use proptest::sample::Index;
use proptest::strategy::ValueTree;
use proptest::test_runner::TestRunner;
const RELATED: [u32; 5] = [0x02, 0x04, 0x20, 0x24, 0x40];
fn listing(bytes: &[u8], flags: u32) -> Result<String, dxbc_reader::Error> {
disassemble_with(bytes, &Options::from_flags(flags).unwrap())
}
fn lines(text: &str) -> Vec<&str> {
text.split('\n').collect()
}
fn comment(line: &str) -> bool {
line.trim_start().starts_with("//")
}
fn relates(plain: &str, flagged: &str, flags: u32, level9: bool) -> Result<(), String> {
let (p, f) = (lines(plain), lines(flagged));
match flags {
0x02 => (p == f).then_some(()).ok_or("0x02 changed the listing".into()),
0x04 | 0x20 | 0x24 => {
if p.len() != f.len() {
return Err(format!("{} lines, {} plain", f.len(), p.len()));
}
for (a, b) in f.iter().zip(&p) {
let prefix = a
.strip_suffix(b)
.ok_or(format!("{a:?} does not end with {b:?}"))?;
if !prefix
.chars()
.all(|c| c == ' ' || c == ':' || c == 'x' || c.is_ascii_hexdigit())
{
return Err(format!("{a:?}: a prefix {prefix:?}"));
}
}
Ok(())
}
0x40 => {
let code = |l: &[&str]| -> Vec<String> {
l.iter()
.filter(|x| !x.trim().is_empty() && !comment(x))
.map(|x| (*x).to_owned())
.collect()
};
let program = f
.first()
.and_then(|version| p.iter().position(|l| l == version))
.map_or(&p[..], |start| &p[start..]);
if level9 || code(program) == code(&f) {
Ok(())
} else {
Err("0x40 changed the program's lines".into())
}
}
0x80 => {
if p.len() != f.len() {
return Err(format!("{} lines, {} plain", f.len(), p.len()));
}
let mut buffer = false;
for (a, b) in f.iter().zip(&p) {
if b.starts_with("dcl_immediateConstantBuffer") {
buffer = true;
}
let literal = b.contains("l(") || b.contains("d(") || b.trim_start().starts_with("def ");
if a != b && !buffer && !literal {
return Err(format!("0x80 changed {b:?} to {a:?}"));
}
if b.trim_end().ends_with("} }") {
buffer = false;
}
}
Ok(())
}
_ => unreachable!("flags {flags:#x}"),
}
}
fn level9(bytes: &[u8]) -> bool {
parts(bytes).is_ok_and(|p| p.iter().any(|c| &c.kind == b"Aon9"))
}
fn programs() -> &'static [(PathBuf, Vec<u8>)] {
static PROGRAMS: OnceLock<Vec<(PathBuf, Vec<u8>)>> = OnceLock::new();
PROGRAMS.get_or_init(|| {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
let mut out = Vec::new();
for dir in ["disasm", "fixtures"] {
for entry in std::fs::read_dir(root.join(dir)).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_some_and(|e| e == "dxbc") {
let bytes = std::fs::read(&path).unwrap();
out.push((path, bytes));
}
}
}
out.sort();
out
})
}
#[test]
fn the_flag_relations_hold_for_microsofts_listings() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/disasm/listings");
let mut checked = 0;
for (path, bytes) in programs() {
let stem = path.file_stem().unwrap().to_str().unwrap();
let plain = std::fs::read_to_string(dir.join(format!("{stem}.asm"))).unwrap();
for flags in RELATED.into_iter().chain([0x80]) {
let flagged = std::fs::read_to_string(dir.join(format!("{stem}.flag{flags:02x}.asm"))).unwrap();
if let Err(why) = relates(&plain, &flagged, flags, level9(bytes)) {
panic!("Microsoft's {stem} under {flags:#04x}: {why}");
}
checked += 1;
}
}
assert_eq!(checked, 6 * programs().len());
}
fn edited() -> impl Strategy<Value = Vec<u8>> {
(
any::<Index>(),
any::<Index>(),
proptest::collection::vec((any::<Index>(), any::<u8>()), 1..4),
)
.prop_map(|(which, part, changes)| {
let programs = programs();
let bytes = &programs[which.index(programs.len())].1;
let directory = parts(bytes).unwrap();
let chunk = directory[part.index(directory.len())];
let start = chunk.data.as_ptr() as usize - bytes.as_ptr() as usize;
let mut out = bytes.clone();
if !chunk.data.is_empty() {
for (at, value) in changes {
out[start + at.index(chunk.data.len())] = value;
}
}
out
})
}
proptest! {
#![proptest_config(ProptestConfig { cases: 256, failure_persistence: None, ..ProptestConfig::default() })]
#[test]
fn an_edited_program_is_listed_or_refused_and_its_listings_relate(bytes in edited()) {
let plain = listing(&bytes, 0);
prop_assert_eq!(&plain, &listing(&bytes, 0), "listed twice, two texts");
match &plain {
Ok(plain) => {
for flags in RELATED.into_iter().chain([0x80, 0x10, 0xf6]) {
let flagged = listing(&bytes, flags);
prop_assert!(flagged.is_ok(), "listed with no flags, refused under {:#04x}: {:?}", flags, flagged);
if !RELATED.contains(&flags) && flags != 0x80 {
continue;
}
if let Err(why) = relates(plain, &flagged.unwrap(), flags, level9(&bytes)) {
prop_assert!(false, "under {:#04x}: {}", flags, why);
}
}
}
Err(_) => {
for flags in [0x02, 0x04, 0x20, 0x24, 0x80, 0xa6] {
prop_assert!(listing(&bytes, flags).is_err(), "refused with no flags, listed under {:#04x}", flags);
}
}
}
}
}
#[test]
fn the_edits_reach_both_outcomes() {
let mut runner = TestRunner::deterministic();
let mut listed = 0;
for _ in 0..256 {
let bytes = edited().new_tree(&mut runner).unwrap().current();
listed += usize::from(listing(&bytes, 0).is_ok());
}
assert!((32..=224).contains(&listed), "{listed} of 256 listed");
}