tokmd-analysis 1.14.0

Analysis logic and enrichers for tokmd receipts.
Documentation
use tokmd_analysis_types::IntegrityReport;
use tokmd_types::FileRow;

pub(super) fn build_integrity_report(rows: &[&FileRow]) -> IntegrityReport {
    let mut sorted_rows = rows.to_vec();
    sorted_rows.sort_unstable_by(|&a, &b| compare_integrity_rows(a, b));

    let mut hasher = blake3::Hasher::new();
    let mut first = true;
    for row in sorted_rows {
        if !first {
            hasher.update(b"\n");
        }
        first = false;
        hasher.update(row.path.as_bytes());
        hasher.update(b":");
        hasher.update(row.bytes.to_string().as_bytes());
        hasher.update(b":");
        hasher.update(row.lines.to_string().as_bytes());
    }

    IntegrityReport {
        algo: "blake3".to_string(),
        hash: hasher.finalize().to_hex().to_string(),
        entries: rows.len(),
    }
}

fn compare_integrity_rows(a: &FileRow, b: &FileRow) -> std::cmp::Ordering {
    let a_bytes = a.path.as_bytes();
    let b_bytes = b.path.as_bytes();
    let min_len = a_bytes.len().min(b_bytes.len());

    let ord = a_bytes[..min_len].cmp(&b_bytes[..min_len]);
    if ord != std::cmp::Ordering::Equal {
        return ord;
    }

    if a_bytes.len() == b_bytes.len() {
        return compare_usize_pair_ascii(a.bytes, a.lines, b.bytes, b.lines);
    }

    if a_bytes.len() < b_bytes.len() {
        b':'.cmp(&b_bytes[min_len])
    } else {
        a_bytes[min_len].cmp(&b':')
    }
}

fn compare_usize_pair_ascii(
    a_bytes: usize,
    a_lines: usize,
    b_bytes: usize,
    b_lines: usize,
) -> std::cmp::Ordering {
    let mut a_buf = [0_u8; 64];
    let mut b_buf = [0_u8; 64];
    let a_len = write_usize_pair_ascii(&mut a_buf, a_bytes, a_lines);
    let b_len = write_usize_pair_ascii(&mut b_buf, b_bytes, b_lines);
    a_buf[..a_len].cmp(&b_buf[..b_len])
}

fn write_usize_pair_ascii(buf: &mut [u8; 64], bytes: usize, lines: usize) -> usize {
    let mut len = write_usize_ascii(&mut buf[..], bytes);
    buf[len] = b':';
    len += 1;
    len + write_usize_ascii(&mut buf[len..], lines)
}

fn write_usize_ascii(buf: &mut [u8], value: usize) -> usize {
    if value == 0 {
        buf[0] = b'0';
        return 1;
    }

    let mut digits = 0;
    let mut n = value;
    while n > 0 {
        digits += 1;
        n /= 10;
    }

    debug_assert!(digits <= buf.len());

    let mut n = value;
    for idx in (0..digits).rev() {
        buf[idx] = b'0' + (n % 10) as u8;
        n /= 10;
    }
    digits
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokmd_types::FileKind;

    fn make_row(path: &str, bytes: usize, lines: usize) -> FileRow {
        FileRow {
            path: path.to_string(),
            module: "mod".to_string(),
            lang: "rust".to_string(),
            kind: FileKind::Parent,
            code: 0,
            comments: 0,
            blanks: 0,
            lines,
            bytes,
            tokens: 0,
        }
    }

    #[test]
    fn compare_integrity_rows_matches_string_sort() {
        let cases = vec![
            ("a", 10, 10, "b", 10, 10),
            ("a", 10, 10, "a", 10, 10),
            ("a", 10, 10, "a", 20, 10),
            ("a", 100, 10, "a", 20, 10),
            ("a", 10, 10, "a.b", 10, 10),
            ("a.b", 10, 10, "a", 10, 10),
            ("foo", 10, 10, "foo.bar", 10, 10),
            ("foo.bar", 10, 10, "foo", 10, 10),
            ("foo", 10, 10, "foo_bar", 10, 10),
            ("a", usize::MAX, 0, "a", 9, usize::MAX),
            ("a", 0, usize::MAX, "a", 0, 9),
            ("a", 999, 0, "a", 1000, 0),
            ("a", 10, 2, "a", 10, 10),
        ];

        for (p1, b1, l1, p2, b2, l2) in cases {
            let r1 = make_row(p1, b1, l1);
            let r2 = make_row(p2, b2, l2);

            let s1 = format!("{p1}:{b1}:{l1}");
            let s2 = format!("{p2}:{b2}:{l2}");
            let expected = s1.cmp(&s2);
            let actual = compare_integrity_rows(&r1, &r2);

            assert_eq!(actual, expected, "Failed for {s1} vs {s2}");
        }
    }
}