acadrust 0.3.3

A pure Rust library for reading and writing CAD files in DXF format (ASCII and Binary) and DWG format (Binary).
Documentation
//! Checksum and magic sequence utilities for DWG format
//!
//! Provides:
//! - **Adler-32 variant**: Page-level checksums for AC18 (R2004+) format
//! - **Magic Sequence**: 256-byte pseudo-random sequence for XOR encryption/padding
//! - **Compression alignment**: Padding calculation for 0x20-byte alignment

/// 256-byte magic sequence generated by an LCG (Linear Congruential Generator).
///
/// Uses the same constants as MSVC `rand()`:
/// `seed = seed * 0x343FD + 0x269EC3`, output = `(seed >> 16) as u8`
///
/// Used for XOR encryption of file header data and padding alignment.
pub fn magic_sequence() -> [u8; 256] {
    let mut seq = [0u8; 256];
    let mut seed: u32 = 1;
    for byte in seq.iter_mut() {
        seed = seed.wrapping_mul(0x343FD).wrapping_add(0x269EC3);
        *byte = (seed >> 16) as u8;
    }
    seq
}

/// Compute the Adler-32 variant checksum used for DWG page checksums.
///
/// This is compatible with `DwgCheckSumCalculator.Calculate()` in ACadSharp.
/// It processes data in chunks of 5552 (0x15B0) bytes to prevent overflow.
///
/// # Arguments
/// * `seed` - Initial checksum value (lower 16 bits = sum1, upper 16 bits = sum2)
/// * `data` - Byte slice to compute checksum over
///
/// # Returns
/// Combined checksum: `(sum2 << 16) | sum1`
pub fn adler32_checksum(seed: u32, data: &[u8]) -> u32 {
    let mut sum1 = seed & 0xFFFF;
    let mut sum2 = seed >> 16;

    // Process in chunks of 5552 to prevent u32 overflow before modulo
    for chunk in data.chunks(5552) {
        for &byte in chunk {
            sum1 += byte as u32;
            sum2 += sum1;
        }
        sum1 %= 0xFFF1;
        sum2 %= 0xFFF1;
    }

    (sum2 << 16) | sum1
}

/// Calculate padding needed to align `length + 0x20` to a 0x20-byte boundary.
///
/// This matches `DwgCheckSumCalculator.CompressionCalculator()` in ACadSharp.
///
/// # Returns
/// Number of padding bytes needed (0–31).
pub fn compression_padding(length: usize) -> usize {
    0x1F - (length + 0x1F) % 0x20
}

/// Apply XOR mask to a buffer using `position ^ 0x4164536B` as a repeating 4-byte pattern.
///
/// This matches `DwgFileHeaderWriterBase.applyMask()` in ACadSharp.
/// The mask is computed ONCE from `0x4164536B ^ stream_position` and applied
/// uniformly to every 4-byte group in the buffer. Buffer length must be a
/// multiple of 4.
pub fn apply_mask(buffer: &mut [u8], stream_position: u64) {
    let mask_value = 0x4164536Bu32 ^ (stream_position as u32);
    let mask = mask_value.to_le_bytes();
    let mut i = 0;
    while i + 4 <= buffer.len() {
        buffer[i] ^= mask[0];
        buffer[i + 1] ^= mask[1];
        buffer[i + 2] ^= mask[2];
        buffer[i + 3] ^= mask[3];
        i += 4;
    }
}

/// Apply the 256-byte magic sequence XOR to an entire buffer.
///
/// This matches `DwgFileHeaderWriterBase.applyMagicSequence()` in ACadSharp.
/// Used for encrypting the 0x6C-byte inner file header in AC18 format.
pub fn apply_magic_sequence(buffer: &mut [u8]) {
    let seq = magic_sequence();
    for (i, byte) in buffer.iter_mut().enumerate() {
        *byte ^= seq[i % 256];
    }
}

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

    #[test]
    fn test_magic_sequence_deterministic() {
        let seq1 = magic_sequence();
        let seq2 = magic_sequence();
        assert_eq!(seq1, seq2);
    }

    #[test]
    fn test_magic_sequence_first_bytes() {
        let seq = magic_sequence();
        // Verify LCG output:
        // seed=1 → 1*0x343FD+0x269EC3 = 0x29D200 → (0x29D200>>16) = 0x29 = 41
        assert_eq!(seq[0], 0x29);
        // Second iteration: 0x29D200*0x343FD+0x269EC3
        // We don't compute manually, but verify it's not zero
        assert_ne!(seq[1], 0);
    }

    #[test]
    fn test_adler32_empty() {
        // With seed 1, sum1=1, sum2=0, empty data → result = (0 << 16) | 1 = 1
        assert_eq!(adler32_checksum(1, &[]), 1);
    }

    #[test]
    fn test_adler32_basic() {
        // Adler-32 of "Wikipedia" with standard seed (0x00010001) = 0x11E60398
        // Our seed format is (sum2 << 16) | sum1, with initial sum1=1, sum2=0 → seed = 1
        let result = adler32_checksum(1, b"Wikipedia");
        assert_eq!(result, 0x11E60398);
    }

    #[test]
    fn test_compression_padding() {
        // 0x1F - (0 + 0x1F) % 0x20 = 31 - 31 = 0
        assert_eq!(compression_padding(0), 0);
        // 0x1F - (1 + 0x1F) % 0x20 = 31 - 0 = 31
        assert_eq!(compression_padding(1), 31);
        // 0x1F - (32 + 0x1F) % 0x20 = 31 - 31 = 0
        assert_eq!(compression_padding(32), 0);
        // 0x1F - (33 + 0x1F) % 0x20 = 31 - 0 = 31
        assert_eq!(compression_padding(33), 31);
    }

    #[test]
    fn test_apply_mask_roundtrip() {
        let original = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
        let mut masked = original.clone();
        apply_mask(&mut masked, 0);
        assert_ne!(masked, original);
        // Apply same mask again to decrypt
        apply_mask(&mut masked, 0);
        assert_eq!(masked, original);
    }

    #[test]
    fn test_apply_magic_sequence_roundtrip() {
        let original = vec![0xAA; 100];
        let mut encrypted = original.clone();
        apply_magic_sequence(&mut encrypted);
        assert_ne!(encrypted, original);
        apply_magic_sequence(&mut encrypted);
        assert_eq!(encrypted, original);
    }
}