use baken_export::anlz::hash::{anlz_dir, AnlzSlots};
use baken_export::anlz::locate::AnlzIndex;
use baken_export::anlz::rewrite::{prepare, FileKind};
use baken_export::anlz::section::AnlzFile;
use baken_export::build::{self, DeviceTrack};
use baken_export::collection::Library;
use baken_export::layout::Layout;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
fn fixture_root() -> Option<PathBuf> {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.claude/fixtures/JPHFAREKORD-20260918");
p.join("PIONEER/rekordbox/export.pdb").exists().then_some(p)
}
fn fixture_tracks(root: &Path) -> Vec<(String, String, String)> {
let json = std::fs::read_to_string(root.join("tracks.json")).unwrap();
let mut out = Vec::new();
for obj in json.split("{\n").skip(1) {
let field = |k: &str| -> String {
let key = format!("\"{k}\": \"");
let start = obj.find(&key).unwrap() + key.len();
let mut s = String::new();
let mut chars = obj[start..].chars();
while let Some(c) = chars.next() {
match c {
'\\' => {
let n = chars.next().unwrap();
match n {
'u' => {
let hex: String = chars.by_ref().take(4).collect();
let cp = u32::from_str_radix(&hex, 16).unwrap();
if (0xD800..0xDC00).contains(&cp) {
let _ = chars.by_ref().take(2).count();
let hex2: String = chars.by_ref().take(4).collect();
let lo = u32::from_str_radix(&hex2, 16).unwrap();
s.push(
char::from_u32(
0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00),
)
.unwrap(),
);
} else {
s.push(char::from_u32(cp).unwrap());
}
}
other => s.push(other),
}
}
'"' => break,
c => s.push(c),
}
}
s
};
out.push((field("title"), field("file_path"), field("analyze_path")));
}
out
}
#[test]
fn hash_matches_every_fixture_track() {
let Some(root) = fixture_root() else { return };
let tracks = fixture_tracks(&root);
assert!(tracks.len() > 500);
let mismatches: Vec<_> = tracks
.iter()
.filter(|(_, fp, ap)| ap.rsplit_once('/').map(|(dir, _)| dir) != Some(&anlz_dir(fp)))
.collect();
assert!(mismatches.is_empty(), "{mismatches:?}");
}
fn mask_pcp2_tails(data: &[u8]) -> Vec<u8> {
let Ok(mut f) = AnlzFile::parse(data) else {
return data.to_vec();
};
for sec in f.sections.iter_mut().filter(|s| &s.tag == b"PCO2") {
let mut off = 20;
while off + 12 <= sec.bytes.len() {
let len = u32::from_be_bytes(sec.bytes[off + 8..off + 12].try_into().unwrap()) as usize;
let end = (off + len).min(sec.bytes.len());
if end > off + 48 {
sec.bytes[off + 48..end].fill(0);
}
off += len.max(12);
}
}
f.to_bytes()
}
#[test]
fn local_anlz_becomes_stick_anlz() {
let Some(root) = fixture_root() else { return };
let local_root = root.join("local-anlz");
if !local_root.is_dir() {
return;
}
let lib = Library::load(&root.join("collection.xml")).unwrap();
let index = AnlzIndex::build(&[local_root]).unwrap();
let by_title: HashMap<&str, Vec<&baken_export::collection::Track>> =
lib.tracks.iter().fold(HashMap::new(), |mut m, t| {
m.entry(t.name.as_str()).or_default().push(t);
m
});
let mut compared = 0;
let mut identical = [0usize; 3];
let mut differ = [0usize; 3];
for (title, file_path, analyze_path) in fixture_tracks(&root) {
let Some(cands) = by_title.get(title.as_str()) else {
continue;
};
if cands.len() != 1 {
continue;
}
let track = cands[0];
let Some(entry) = index.find(track) else {
continue;
};
compared += 1;
let bpm = track.tempos.first().map(|t| t.bpm).unwrap_or(0.0);
for (i, kind) in FileKind::ALL.iter().enumerate() {
let local = entry.sibling(kind.extension());
let stick = root
.join(analyze_path.trim_start_matches('/'))
.with_extension(kind.extension());
let (Ok(l), Ok(s)) = (std::fs::read(&local), std::fs::read(&stick)) else {
continue;
};
let mut file = AnlzFile::parse(&l).unwrap();
prepare(&mut file, *kind, &file_path, &track.cues, bpm);
let ours = file.to_bytes();
if mask_pcp2_tails(&ours) == mask_pcp2_tails(&s) {
identical[i] += 1;
} else {
differ[i] += 1;
if differ[i] <= 4 {
let theirs = AnlzFile::parse(&s).unwrap();
for (a, b) in file.sections.iter().zip(theirs.sections.iter()) {
if a.bytes != b.bytes {
let at = a
.bytes
.iter()
.zip(&b.bytes)
.position(|(x, y)| x != y)
.unwrap_or(a.bytes.len().min(b.bytes.len()));
eprintln!(" {} {kind:?} {}: ours len {} theirs len {} first diff at 0x{at:x}: ours {:02x?} theirs {:02x?}", track.name, a.tag_str(), a.bytes.len(), b.bytes.len(), &a.bytes[at.min(a.bytes.len())..(at + 12).min(a.bytes.len())], &b.bytes[at.min(b.bytes.len())..(at + 12).min(b.bytes.len())]);
}
}
if file.sections.len() != theirs.sections.len() {
eprintln!(
" {} {kind:?}: section count {} vs {}",
track.name,
file.sections.len(),
theirs.sections.len()
);
}
}
}
}
}
eprintln!(
"compared {compared} tracks; identical DAT/EXT/2EX = {identical:?}, different = {differ:?}"
);
assert!(compared > 250);
for i in 0..3 {
assert!(
identical[i] * 100 / (identical[i] + differ[i]) >= 90,
"kind {i}: {identical:?} vs {differ:?}"
);
}
}
#[test]
fn pdb_from_fixture_collection_round_trips() {
let Some(root) = fixture_root() else { return };
let lib = Library::load(&root.join("collection.xml")).unwrap();
let names = ["Hard Techno", "Non Hard Techno", "HT-70min", "Openings"];
let selected: Vec<usize> = names
.iter()
.map(|n| {
lib.playlists
.iter()
.position(|p| p.path == *n && !p.is_folder)
.unwrap()
})
.collect();
let mut layout = Layout::default();
let mut slots = AnlzSlots::default();
let mut seen = std::collections::HashSet::new();
let mut tracks = Vec::new();
for &pi in &selected {
for &tid in &lib.playlists[pi].track_ids {
if !seen.insert(tid) {
continue;
}
let t = lib.track(tid).unwrap().clone();
let usb_path = layout.assign(&t);
let (anlz_dir, anlz_index) = slots.assign(&usb_path);
tracks.push(DeviceTrack {
anlz_dir,
anlz_index,
usb_path,
file_size: t.size,
sample_depth: 16,
file_type: build::file_type_for(&t.kind, t.file_name()),
bitrate: t.bit_rate,
sample_rate: t.sample_rate,
track: t,
});
}
}
let theirs: HashMap<String, String> = fixture_tracks(&root)
.into_iter()
.map(|(_, fp, ap)| (fp, ap))
.collect();
let compared: Vec<_> = tracks
.iter()
.filter_map(|dt| Some((theirs.get(&dt.usb_path)?, dt.anlz_path("DAT"))))
.collect();
assert!(compared.len() > 500);
assert!(compared.iter().all(|(a, b)| *a == b));
assert_eq!(
compared
.iter()
.filter(|(a, _)| a.ends_with("ANLZ0001.DAT"))
.count(),
1
);
let model = build::build(&lib, &tracks, &selected, "JPHFA-REKORD", "2026-09-18");
assert_eq!(model.playlists.len(), 4);
let bytes = baken_export::pdb::write(&model);
assert_eq!(bytes.len() % 4096, 0);
let page = |i: usize| &bytes[i * 4096..(i + 1) * 4096];
let mut counts = HashMap::new();
for i in 0..20 {
let o = 0x1c + 16 * i;
let w = |k: usize| {
u32::from_le_bytes(bytes[o + 4 * k..o + 4 * k + 4].try_into().unwrap()) as usize
};
let (ty, _empty, first, last) = (w(0), w(1), w(2), w(3));
let mut p = first;
loop {
let pg = page(p);
if pg[0x1b] == 0x24 {
let n = pg[0x18] as usize + 0x100 * (pg[0x19] & 1) as usize;
*counts.entry(ty).or_insert(0) += n;
if ty == 19 {
let c = u32::from_le_bytes(pg[0x2c..0x30].try_into().unwrap());
assert_eq!(c as usize, tracks.len());
}
for r in 0..n {
let base = 4096 - (r / 16) * 36;
let at = base - 6 - 2 * (r % 16);
let off = u16::from_le_bytes([pg[at], pg[at + 1]]) as usize;
assert_eq!(off % 4, 0);
assert!(0x28 + off < at);
}
}
if p == last {
break;
}
p = u32::from_le_bytes(pg[0x0c..0x10].try_into().unwrap()) as usize;
}
}
assert_eq!(counts[&0], tracks.len());
assert_eq!(
counts[&8],
selected
.iter()
.map(|&i| lib.playlists[i].track_ids.len())
.sum::<usize>()
);
assert_eq!(counts[&7], 4);
assert_eq!(counts[&5], 24);
assert_eq!(counts[&6], 8);
assert_eq!(counts[&16], 27);
assert!(counts[&2] > 100 && counts[&3] > 50 && counts[&1] > 5);
eprintln!(
"tracks {} artists {} albums {} genres {} labels {} pages {}",
counts[&0],
counts[&2],
counts[&3],
counts[&1],
counts[&4],
bytes.len() / 4096
);
}