ycd-reader 0.1.0

A Rust library for random-access and sequential reading of y-cruncher YCD digit files
Documentation
#![allow(dead_code)]

use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use ycd_reader::{YcdFileUtil, YcdHeaderInfoElem, YcdMultiFileStream, YcdSeqBlockStream};

const GOLDEN_PATH: &str = "tests/ycd/Pi - Dec - Chudnovsky_3000000.txt";
const ONE_MILLION_BASE: &str = "tests/ycd/1000000/Pi - Dec - Chudnovsky - ";
const TWO_MILLION_BASE: &str = "tests/ycd/2000000/Pi - Dec - Chudnovsky - ";

static TEMP_FILE_ID: AtomicU64 = AtomicU64::new(0);

pub struct TempYcd {
    pub path: PathBuf,
}

impl TempYcd {
    pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
        let id = TEMP_FILE_ID.fetch_add(1, Ordering::Relaxed);
        let path =
            std::env::temp_dir().join(format!("ycd-reader_{}_{}.ycd", std::process::id(), id));
        fs::write(&path, bytes)?;
        Ok(Self { path })
    }

    pub fn valid(
        digits: &str,
        block_size: usize,
        block_id: usize,
        line_ending: &str,
        extra_header: &str,
    ) -> io::Result<Self> {
        assert_eq!(digits.len(), block_size);
        let header = format!(
            "#Compressed Digit File{e}{e}\
             FileVersion:\t1.1.0{e}{e}\
             Base:\t10{e}{e}\
             FirstDigits:\t3.14159265358979323846{e}{e}\
             TotalDigits:\t0{e}{e}\
             {extra_header}\
             Blocksize:\t{block_size}{e}\
             BlockID:\t{block_id}{e}{e}\
             EndHeader{e}{e}",
            e = line_ending
        );
        let mut bytes = header.into_bytes();
        bytes.push(0);
        bytes.extend(encode_digits(digits));
        Self::from_bytes(&bytes)
    }

    pub fn final_block(
        digits: &str,
        block_size: usize,
        block_id: usize,
        total_digits: usize,
    ) -> io::Result<Self> {
        let header = format!(
            "FileVersion: 1.1.0\n\
             Base: 10\n\
             FirstDigits: 3.14159265358979323846\n\
             TotalDigits: {total_digits}\n\
             Blocksize: {block_size}\n\
             BlockID: {block_id}\n\
             EndHeader\n\n\0"
        );
        let mut bytes = header.into_bytes();
        bytes.extend(encode_digits(digits));
        Self::from_bytes(&bytes)
    }
}

impl Drop for TempYcd {
    fn drop(&mut self) {
        let _ = fs::remove_file(&self.path);
    }
}

pub fn one_million_path(block_id: usize) -> String {
    format!("{ONE_MILLION_BASE}{block_id}.ycd")
}

pub fn two_million_path(block_id: usize) -> String {
    format!("{TWO_MILLION_BASE}{block_id}.ycd")
}

pub fn golden_digits() -> String {
    fs::read_to_string(GOLDEN_PATH).expect("golden digit file must be readable")
}

fn encode_digits(digits: &str) -> Vec<u8> {
    let mut encoded = Vec::new();
    for chunk in digits.as_bytes().chunks(19) {
        let mut block = String::from_utf8(chunk.to_vec()).expect("test digits are ASCII");
        while block.len() < 19 {
            block.push('0');
        }
        let number = block.parse::<u64>().expect("test block must fit in u64");
        encoded.extend(number.to_le_bytes());
    }
    encoded
}

pub fn collect_single(path: impl AsRef<Path>, unit_size: i32) -> io::Result<String> {
    let header = YcdFileUtil::get_ycd_header(path.as_ref())?;
    let block_size = header[&YcdHeaderInfoElem::Blocksize]
        .parse::<i64>()
        .unwrap();
    let block_id = header[&YcdHeaderInfoElem::BlockID].parse::<i64>().unwrap();
    let mut expected_start = 1 + block_size * block_id;
    let mut expected_process_no = 1;
    let mut result = String::new();
    let mut stream = YcdSeqBlockStream::new(path, unit_size)?;

    while stream.has_next() {
        let unit = stream.next()?.clone();
        assert_eq!(unit.process_no, expected_process_no);
        assert_eq!(unit.start_digit, expected_start);
        expected_process_no += 1;
        expected_start += unit.value.len() as i64;
        result.push_str(&unit.value);
    }

    assert_eq!(
        stream.next().unwrap_err().kind(),
        io::ErrorKind::UnexpectedEof
    );
    Ok(result)
}

pub fn collect_single_prefix(
    path: impl AsRef<Path>,
    unit_size: i32,
    length: usize,
) -> io::Result<String> {
    let mut result = String::new();
    let mut stream = YcdSeqBlockStream::new(path, unit_size)?;
    while result.len() < length {
        result.push_str(&stream.next()?.value);
    }
    result.truncate(length);
    Ok(result)
}

pub fn collect_multi<P: AsRef<Path>>(paths: &[P], unit_size: i32) -> io::Result<String> {
    let first_header = YcdFileUtil::get_ycd_header(paths[0].as_ref())?;
    let block_size = first_header[&YcdHeaderInfoElem::Blocksize]
        .parse::<i64>()
        .unwrap();
    let block_id = first_header[&YcdHeaderInfoElem::BlockID]
        .parse::<i64>()
        .unwrap();
    let mut expected_start = 1 + block_size * block_id;
    let mut expected_process_no = 1;
    let mut result = String::new();
    let mut stream = YcdMultiFileStream::new(paths, unit_size)?;

    while stream.has_next() {
        let unit = stream.next()?.clone();
        assert_eq!(unit.process_no, expected_process_no);
        assert_eq!(unit.start_digit, expected_start);
        expected_process_no += 1;
        expected_start += unit.value.len() as i64;
        result.push_str(&unit.value);
    }

    assert_eq!(
        stream.next().unwrap_err().kind(),
        io::ErrorKind::UnexpectedEof
    );
    Ok(result)
}

pub fn collect_single_from(
    path: impl AsRef<Path>,
    unit_size: i32,
    start_position: i64,
) -> io::Result<String> {
    let mut result = String::new();
    let mut stream = YcdSeqBlockStream::new_from(path, unit_size, start_position)?;
    let mut expected_start = start_position;
    let mut expected_process_no: i64 = 1;

    while stream.has_next() {
        let unit = stream.next()?.clone();
        assert_eq!(unit.process_no, expected_process_no);
        assert_eq!(unit.start_digit, expected_start);
        expected_process_no += 1;
        expected_start += unit.value.len() as i64;
        result.push_str(&unit.value);
    }
    assert_eq!(
        stream.next().unwrap_err().kind(),
        io::ErrorKind::UnexpectedEof
    );
    Ok(result)
}

pub fn collect_multi_from<P: AsRef<Path>>(
    paths: &[P],
    unit_size: i32,
    start_position: i64,
) -> io::Result<String> {
    let mut result = String::new();
    let mut stream = YcdMultiFileStream::new_from(paths, unit_size, start_position)?;
    let mut expected_start = start_position;
    let mut expected_process_no: i64 = 1;

    while stream.has_next() {
        let unit = stream.next()?.clone();
        assert_eq!(unit.process_no, expected_process_no);
        assert_eq!(unit.start_digit, expected_start);
        expected_process_no += 1;
        expected_start += unit.value.len() as i64;
        result.push_str(&unit.value);
    }
    assert_eq!(
        stream.next().unwrap_err().kind(),
        io::ErrorKind::UnexpectedEof
    );
    Ok(result)
}