ycd-reader 0.1.0

A Rust library for random-access and sequential reading of y-cruncher YCD digit files
Documentation
mod common;

use std::io;

use common::{collect_single, one_million_path, two_million_path, TempYcd};
use ycd_reader::{YcdFileUtil, YcdHeaderInfoElem, YcdSeqBlockStream};

#[test]
fn real_headers_are_parsed_at_the_exact_data_offset() {
    for block_id in [0, 1, 14] {
        let path = one_million_path(block_id);
        let expected_size = if block_id < 10 { 195 } else { 196 };
        assert_eq!(YcdFileUtil::get_header_size(&path).unwrap(), expected_size);

        let header = YcdFileUtil::get_ycd_header(&path).unwrap();
        assert_eq!(header[&YcdHeaderInfoElem::FileVersion], "1.1.0");
        assert_eq!(header[&YcdHeaderInfoElem::Base], "10");
        assert_eq!(header[&YcdHeaderInfoElem::Blocksize], "1000000");
        assert_eq!(header[&YcdHeaderInfoElem::BlockID], block_id.to_string());
    }

    for block_id in [0, 2] {
        let path = two_million_path(block_id);
        assert_eq!(YcdFileUtil::get_header_size(&path).unwrap(), 195);
        assert_eq!(
            YcdFileUtil::get_ycd_header(&path).unwrap()[&YcdHeaderInfoElem::Blocksize],
            "2000000"
        );
    }
}

#[test]
fn generated_file_preserves_leading_zeroes_and_a_partial_final_block() {
    let digits = concat!("0000000000000000001", "2345678901234567890", "00420");
    let file = TempYcd::valid(digits, digits.len(), 0, "\r\n", "").unwrap();
    let mut stream = YcdSeqBlockStream::new(&file.path, 20).unwrap();

    let first = stream.next().unwrap().clone();
    let second = stream.next().unwrap().clone();
    let third = stream.next().unwrap().clone();

    assert_eq!(first.value, &digits[..20]);
    assert_eq!(second.value, &digits[20..40]);
    assert_eq!(third.value, &digits[40..]);
    assert_eq!((first.process_no, first.start_digit), (1, 1));
    assert_eq!((second.process_no, second.start_digit), (2, 21));
    assert_eq!((third.process_no, third.start_digit), (3, 41));
    assert!(!stream.has_next());
}

#[test]
fn exact_multiple_of_nineteen_stops_without_reading_an_extra_block() {
    let digits = "12345678901234567890123456789012345678";
    let file = TempYcd::valid(digits, digits.len(), 0, "\n", "").unwrap();

    assert_eq!(collect_single(&file.path, 19).unwrap(), digits);
}

#[test]
fn total_digits_limits_a_short_final_file() {
    let digits = "123456789012345678901234";
    let file = TempYcd::final_block(digits, 38, 0, digits.len()).unwrap();

    assert_eq!(collect_single(&file.path, 19).unwrap(), digits);
}

#[test]
fn long_headers_and_lf_line_endings_are_supported() {
    let extra_header = format!("UnknownField:\t{}\n", "x".repeat(400));
    let digits = "1234567890123456789";
    let file = TempYcd::valid(digits, digits.len(), 0, "\n", &extra_header).unwrap();

    assert!(YcdFileUtil::get_header_size(&file.path).unwrap() > 300);
    assert_eq!(collect_single(&file.path, 19).unwrap(), digits);
}

#[test]
fn invalid_inputs_and_headers_return_specific_errors() {
    assert_eq!(
        YcdSeqBlockStream::new("does-not-exist.ycd", 18)
            .unwrap_err()
            .kind(),
        io::ErrorKind::InvalidInput
    );
    assert_eq!(
        YcdSeqBlockStream::new("does-not-exist.ycd", 19)
            .unwrap_err()
            .kind(),
        io::ErrorKind::NotFound
    );

    let invalid_headers = [
        "FileVersion: 1.1.0\nBase: 10\nFirstDigits: 3.14\nTotalDigits: 0\nBlockID: 0\nEndHeader\n\n\0",
        "FileVersion: 1.1.0\nBase: 10\nFirstDigits: 3.14\nTotalDigits: 0\nBlocksize: x\nBlockID: 0\nEndHeader\n\n\0",
        "FileVersion: 1.1.0\nBase: 16\nFirstDigits: 3.14\nTotalDigits: 0\nBlocksize: 19\nBlockID: 0\nEndHeader\n\n\0",
        "FileVersion: 1.1.0\nBase: 10\nFirstDigits: 3.14\nTotalDigits: 0\nBlocksize: 19\nBlockID: -1\nEndHeader\n\n\0",
        "FileVersion: 1.1.0\nBase: 10\nFirstDigits: 3.14\nTotalDigits: 0\nBlocksize: 19\nBlockID: 0\n",
        "FileVersion: 1.1.0\nBase: 10\nFirstDigits: 3.14\nTotalDigits: 0\nBlocksize: 19\nBlockID: 0\nEndHeader\n\nX",
    ];

    for header in invalid_headers {
        let file = TempYcd::from_bytes(header.as_bytes()).unwrap();
        assert_eq!(
            YcdFileUtil::get_ycd_header(&file.path).unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
    }
}

#[test]
fn corrupt_payloads_are_rejected() {
    let header = concat!(
        "FileVersion: 1.1.0\n",
        "Base: 10\n",
        "FirstDigits: 3.14\n",
        "TotalDigits: 0\n",
        "Blocksize: 38\n",
        "BlockID: 0\n",
        "EndHeader\n\n\0"
    );
    let mut truncated = header.as_bytes().to_vec();
    truncated.extend(123_u64.to_le_bytes());
    let file = TempYcd::from_bytes(&truncated).unwrap();
    let mut stream = YcdSeqBlockStream::new(&file.path, 38).unwrap();
    assert_eq!(
        stream.next().unwrap_err().kind(),
        io::ErrorKind::UnexpectedEof
    );

    let mut oversized = header
        .replace("Blocksize: 38", "Blocksize: 19")
        .into_bytes();
    oversized.extend(u64::MAX.to_le_bytes());
    let file = TempYcd::from_bytes(&oversized).unwrap();
    let mut stream = YcdSeqBlockStream::new(&file.path, 19).unwrap();
    assert_eq!(
        stream.next().unwrap_err().kind(),
        io::ErrorKind::InvalidData
    );
}